gdharness 1.2.39 → 1.2.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cli.js +1435 -1410
- package/build/godot/addons/gdharness_editor/animation_reads.gd +11 -2
- package/build/godot/addons/gdharness_editor/bridge_client.gd +2 -1
- package/build/godot/addons/gdharness_editor/headless_check.gd +22 -11
- package/build/godot/addons/gdharness_editor/property_values.gd +1 -1
- package/build/godot/addons/gdharness_editor/reading.gd +15 -0
- package/build/godot/addons/gdharness_editor/scene_file.gd +9 -0
- package/build/godot/addons/gdharness_editor/tools/animation_tools.gd +39 -5
- package/build/godot/addons/gdharness_editor/tools/resource_tools.gd +68 -15
- package/build/godot/addons/gdharness_editor/tools/scene_tools.gd +12 -12
- package/build/godot/addons/gdharness_runtime/reading.gd +15 -0
- package/build/godot/addons/gdharness_runtime/runtime_autoload.gd +3 -1
- package/build/godot/addons/gdharness_runtime/runtime_changes.gd +2 -2
- package/build/godot/addons/gdharness_runtime/runtime_input.gd +15 -2
- package/build/godot/addons/gdharness_runtime/runtime_items.gd +175 -0
- package/build/godot/addons/gdharness_runtime/runtime_items.gd.uid +1 -0
- package/build/godot/addons/gdharness_runtime/runtime_paths.gd +43 -10
- package/build/godot/addons/gdharness_runtime/runtime_targets.gd +61 -20
- package/build/godot/addons/gdharness_runtime/runtime_values.gd +3 -1
- package/build/godot/addons/gdharness_runtime/runtime_words.gd +48 -18
- package/build/godot/operations/animation_reads.gd +11 -2
- package/build/godot/operations/editor_tools.gd +9 -0
- package/build/godot/operations/godot_operations.gd +2 -1
- package/build/godot/operations/property_values.gd +1 -1
- package/build/godot/operations/reading.gd +15 -0
- package/build/index.js +2789 -2765
- package/package.json +1 -1
package/build/cli.js
CHANGED
|
@@ -8577,512 +8577,1127 @@ var init_scratch = __esm(() => {
|
|
|
8577
8577
|
];
|
|
8578
8578
|
});
|
|
8579
8579
|
|
|
8580
|
-
// src/
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
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
|
-
|
|
8616
|
-
|
|
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
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
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
|
|
8657
|
-
const
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
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
|
-
|
|
8696
|
+
if (open.length !== 1) {
|
|
8697
|
+
throw new MalformedReportError(`<${current().name}> is never closed`);
|
|
8698
|
+
}
|
|
8699
|
+
return root;
|
|
8665
8700
|
}
|
|
8666
|
-
function
|
|
8667
|
-
const
|
|
8668
|
-
return
|
|
8701
|
+
function count(attributes, name) {
|
|
8702
|
+
const value = Number(attributes[name] ?? "0");
|
|
8703
|
+
return Number.isFinite(value) ? value : 0;
|
|
8669
8704
|
}
|
|
8670
|
-
|
|
8671
|
-
const
|
|
8672
|
-
const
|
|
8673
|
-
|
|
8674
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8699
|
-
const
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
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
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
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
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
const
|
|
8733
|
-
|
|
8734
|
-
|
|
8735
|
-
|
|
8736
|
-
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
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
|
|
8805
|
-
|
|
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
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
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
|
|
8772
|
+
});
|
|
8773
|
+
const all = suites.flatMap((suite) => suite.cases);
|
|
8814
8774
|
return {
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
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
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
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
|
|
8854
|
-
const
|
|
8855
|
-
if (
|
|
8856
|
-
return
|
|
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
|
|
8865
|
-
|
|
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
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
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
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
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
|
-
|
|
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
|
|
8908
|
-
|
|
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
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
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
|
-
|
|
8850
|
+
take(printed.slice(last));
|
|
8851
|
+
return { merged, actual };
|
|
8932
8852
|
}
|
|
8933
|
-
|
|
8934
|
-
const
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
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
|
-
|
|
8940
|
-
|
|
8897
|
+
}
|
|
8898
|
+
function shortened(detail) {
|
|
8899
|
+
if (detail === null || detail.length <= LONGEST_DETAIL) {
|
|
8900
|
+
return detail;
|
|
8941
8901
|
}
|
|
8942
|
-
|
|
8943
|
-
|
|
8944
|
-
|
|
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
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8951
|
-
|
|
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
|
-
|
|
8953
|
+
spent += size;
|
|
8954
|
+
whole.add(index);
|
|
8954
8955
|
}
|
|
8955
|
-
const
|
|
8956
|
-
|
|
8957
|
-
|
|
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
|
-
|
|
8960
|
-
|
|
8994
|
+
return {
|
|
8995
|
+
total: total ?? suites.reduce((sum, suite) => sum + suite.orphans, 0),
|
|
8996
|
+
suites
|
|
8997
|
+
};
|
|
8961
8998
|
}
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
const
|
|
8980
|
-
if (
|
|
8981
|
-
|
|
8982
|
-
|
|
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
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
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
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9008
|
-
|
|
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
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
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
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
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
|
-
|
|
9020
|
-
|
|
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
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
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
|
|
9031
|
-
var
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
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/
|
|
9040
|
-
import { readFileSync as
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
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
|
|
9196
|
+
return result;
|
|
9046
9197
|
}
|
|
9047
|
-
function
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
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
|
|
9055
|
-
|
|
9056
|
-
|
|
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
|
|
9071
|
-
return
|
|
9221
|
+
function problemsIn(log) {
|
|
9222
|
+
return problemsAmong(log.everything());
|
|
9072
9223
|
}
|
|
9073
|
-
function
|
|
9074
|
-
return
|
|
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
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
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
|
|
9084
|
-
if (
|
|
9085
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
10890
|
+
return existsSync12(join13(directory, ".gdignore"));
|
|
10276
10891
|
}
|
|
10277
10892
|
function installedByUs(directory) {
|
|
10278
|
-
return existsSync12(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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
|
|
11577
|
+
return join14(dirname7(fileURLToPath(import.meta.url)), "godot", "addons");
|
|
10963
11578
|
}
|
|
10964
11579
|
function shippedOperationsScript() {
|
|
10965
|
-
return
|
|
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 =
|
|
10971
|
-
if (!existsSync13(
|
|
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 =
|
|
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(
|
|
11594
|
+
writeFileSync6(join14(target, VERSION_MARKER), `${SERVER_VERSION}
|
|
10980
11595
|
`);
|
|
10981
11596
|
installed.push({ name, path: target, replaced });
|
|
10982
11597
|
}
|
|
10983
|
-
writeFileSync6(
|
|
11598
|
+
writeFileSync6(join14(projectPath, "addons", ADDONS[0], DIGEST_MARKER), `${editorAddonDigest(from)}
|
|
10984
11599
|
`);
|
|
10985
|
-
writeFileSync6(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
11084
|
-
const 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(
|
|
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(
|
|
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,
|
|
11874
|
+
var PROJECT_INFO_SECTIONS, PROJECT_PATH, RUNNING_PROJECT_PATH, RUNNING_PID, SCENE_PATH, WITHOUT_AN_EDITOR = "Through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy, or readBy for a read. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads or as a property is set is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.", WHOLE_NUMBERS = 'A whole number of 2^53 or more, such as a 64-bit seed or a resource uid, is written as its digits, {"_type": "int", "value": "9007199254740993"}, the form a read answers with: a JSON number that large can arrive as a neighbour of the one sent, so sent as a number it is refused.', SCRIPT_PATH, RESOURCE_FILE = 'File inside the project, such as "sprites/hero.png" or "materials/steel.tres".', RESOURCE_PATH, NODE_PATH, PROPERTIES, XY, ANIMATION_TRACK, SCRIPT_MODIFICATIONS, INPUT_EVENTS, TILESET_SOURCES, TILEMAP_CELLS, COLOUR2, TOOL_SPECS, SPECS_BY_NAME;
|
|
11260
11875
|
var init_tool_definitions = __esm(() => {
|
|
11261
11876
|
init_launch();
|
|
11262
11877
|
PROJECT_INFO_SECTIONS = [
|
|
@@ -11310,7 +11925,10 @@ var init_tool_definitions = __esm(() => {
|
|
|
11310
11925
|
type: { type: "string", enum: ["property", "method"] },
|
|
11311
11926
|
nodePath: { type: "string", description: "Target node, relative to the AnimationPlayer root." },
|
|
11312
11927
|
property: { type: "string", description: "For property tracks: the property to animate." },
|
|
11313
|
-
method: {
|
|
11928
|
+
method: {
|
|
11929
|
+
type: "string",
|
|
11930
|
+
description: "For method tracks: the method to call. The answer's methodFound says whether the target node has it; a method it lacks is still added, since it may be written next, and until it exists each call the track makes fails."
|
|
11931
|
+
},
|
|
11314
11932
|
keyframes: {
|
|
11315
11933
|
type: "array",
|
|
11316
11934
|
items: {
|
|
@@ -11412,7 +12030,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11412
12030
|
additionalProperties: false
|
|
11413
12031
|
}
|
|
11414
12032
|
};
|
|
11415
|
-
|
|
12033
|
+
COLOUR2 = {
|
|
11416
12034
|
type: "object",
|
|
11417
12035
|
description: "set_theme_color: r, g and b from 0 to 1, with a optional and opaque by default.",
|
|
11418
12036
|
properties: { r: { type: "number" }, g: { type: "number" }, b: { type: "number" }, a: { type: "number" } },
|
|
@@ -11725,7 +12343,8 @@ var init_tool_definitions = __esm(() => {
|
|
|
11725
12343
|
name: "project_test",
|
|
11726
12344
|
title: "Run tests",
|
|
11727
12345
|
effect: "additive",
|
|
11728
|
-
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted
|
|
12346
|
+
description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted, not listed, and an engine message keeps the frames above gdUnit4, not the twenty inside it; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, away from a player's saves; on macOS the engine reads user:// off HOME, so it writes where the player keeps theirs, and savesNote says so. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. An autoload that does not compile stops the run at once, its errors listed the same way. A client sending a progress token is told as each suite finishes, and at least every twenty seconds, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
|
|
12347
|
+
answer: "time is gdUnit4's own sum of the seconds its cases took, from its report. tookMs is the whole call in milliseconds, from the class rebuild to the engine's exit, which is what a caller waits for.",
|
|
11729
12348
|
parameters: {
|
|
11730
12349
|
projectPath: PROJECT_PATH,
|
|
11731
12350
|
path: {
|
|
@@ -11891,7 +12510,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11891
12510
|
{
|
|
11892
12511
|
name: "scene_signal",
|
|
11893
12512
|
title: "Scene signals",
|
|
11894
|
-
description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connect of a pair already connected says alreadyConnected, with flagsBefore when it changed the flags. ${WITHOUT_AN_EDITOR}`,
|
|
12513
|
+
description: `Signal connections in a scene file. Each connection answers with its flags as the number and as flagNames; persist is on every connection, since a scene saves no other kind. A connect of a pair already connected says alreadyConnected, with flagsBefore when it changed the flags. A connect to a method the target does not have is still written, as the editor's own dialog allows, since the method may be written next; methodFound false says so, and until the method exists the signal fails each time it is emitted. list marks editable false on a connection an instanced scene makes inside itself, which only that scene can change. ${WITHOUT_AN_EDITOR}`,
|
|
11895
12514
|
parameters: {
|
|
11896
12515
|
projectPath: PROJECT_PATH,
|
|
11897
12516
|
scenePath: SCENE_PATH,
|
|
@@ -11936,7 +12555,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11936
12555
|
{
|
|
11937
12556
|
name: "scene_animation",
|
|
11938
12557
|
title: "Animations",
|
|
11939
|
-
description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write
|
|
12558
|
+
description: "Animations in an AnimationPlayer and states in an AnimationTree state machine. read answers with every track of an animation in the shape add_track takes, so a track read off one animation can be added to another. read and read_states need no editor: one serving the project answers and says whether it holds unsaved changes to the scene, and with none a headless engine reads the file without running the scene's scripts. The ops that write go through the editor when one serves the project; with none, a headless engine runs the editor's own tool and the answer says so under writtenBy. That engine runs every script of the file, where the editor runs only tool scripts, so what a script changes as the file loads is put back before anything is saved, and a file where that cannot be done, such as a scene whose script does not compile, is refused and nothing is written. An editor of the project that is running and has not reached this server is waited for rather than written around.",
|
|
11940
12559
|
parameters: {
|
|
11941
12560
|
projectPath: PROJECT_PATH,
|
|
11942
12561
|
scenePath: SCENE_PATH,
|
|
@@ -11948,7 +12567,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
11948
12567
|
animationName: {
|
|
11949
12568
|
type: "string",
|
|
11950
12569
|
ops: ["read", "create", "add_track", "add_state"],
|
|
11951
|
-
description: "The animation in the player. read: the one to answer with whole; without it, read lists them. add_state: the one that state plays."
|
|
12570
|
+
description: "The animation in the player. read: the one to answer with whole, its tracks a list; without it, read lists them, each with the number of its tracks under tracks. add_state: the one that state plays."
|
|
11952
12571
|
},
|
|
11953
12572
|
length: { type: "number", ops: ["create"], description: "create: seconds. Default 1." },
|
|
11954
12573
|
loopMode: {
|
|
@@ -12203,20 +12822,24 @@ var init_tool_definitions = __esm(() => {
|
|
|
12203
12822
|
controlType: {
|
|
12204
12823
|
type: "string",
|
|
12205
12824
|
ops: ["set_theme_color", "set_theme_font_size"],
|
|
12206
|
-
description: 'Theme ops: the Control class
|
|
12825
|
+
description: 'Theme ops: the theme type, a Control or Window class such as "Button", or the name of a type variation, which can be any name. A name that is no such class is set as a variation and answered with a note saying so.'
|
|
12207
12826
|
},
|
|
12208
12827
|
colorName: {
|
|
12209
12828
|
type: "string",
|
|
12210
12829
|
ops: ["set_theme_color"],
|
|
12211
12830
|
description: 'set_theme_color: such as "font_color".'
|
|
12212
12831
|
},
|
|
12213
|
-
color: { ...
|
|
12832
|
+
color: { ...COLOUR2, ops: ["set_theme_color"] },
|
|
12214
12833
|
fontSizeName: {
|
|
12215
12834
|
type: "string",
|
|
12216
12835
|
ops: ["set_theme_font_size"],
|
|
12217
12836
|
description: 'set_theme_font_size: such as "font_size".'
|
|
12218
12837
|
},
|
|
12219
|
-
size: {
|
|
12838
|
+
size: {
|
|
12839
|
+
type: "number",
|
|
12840
|
+
ops: ["set_theme_font_size"],
|
|
12841
|
+
description: "set_theme_font_size: pixels, a whole number of 1 or more."
|
|
12842
|
+
}
|
|
12220
12843
|
},
|
|
12221
12844
|
requires: ["projectPath", "resourcePath"],
|
|
12222
12845
|
operations: {
|
|
@@ -12421,7 +13044,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12421
13044
|
requires: []
|
|
12422
13045
|
}
|
|
12423
13046
|
},
|
|
12424
|
-
answer: "stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The runtime addon's own functions, gdharness answering its tools inside the game, are not among them: their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
|
|
13047
|
+
answer: "start: through is gdharness for a game this server started itself, and editor for one the editor plays. scene is the scene the game started in, the project's main scene when none was given. refreshedClasses names the class_name declarations on disk that the engine's class cache was missing, which were added to it before the game started so the game could resolve them; it is empty when the cache was current. stop: endedPid is, for a run the editor plays, the number its game announced under, read at the stop rather than remembered, and the operating system hands a freed number to the next process, so it can equal the game before it. stopped is false, with the number under notSignalled, when that number no longer answers as the process the run was started as and nothing was signalled, the note naming which property it failed on, and false with the number under stillGoing when what was told to end was still running ten seconds later. endedThrough says how the stop ended a run that was going: close (the game was asked to close and exited, so exitCode is its own), keeper (the process that started the game ended it through the handle it holds), signal (its pid was signalled once shown to still be the run), or editor (the editor playing it was asked). A start that ends the run before it says how under endedPreviousRunThrough, with the same values, and one that refused to signal that run names the property it failed on under previousRunLeftBecause. check: hung is a run killed at timeoutMs, and errors and warnings count what it printed, which comes back under entries with repeats folded together. entries holds the errors and warnings and not the ordinary lines the run printed, which printedLinesNotShown counts; a check given --verbose holds those too, and editor_run start then editor_output reads everything. An engine crash is an error whatever it prints after: the crash handler's report is the entry, and the engine version and the native and GDScript backtraces are its detail, so a crashed run is never booted or clean. arguments is the engine command line the check ran, as a start's is. profile: per function its script, the function (Class.name when the script declares a class_name), the line its body's first statement is on, for a lambda the named function it is written in (within), calls, and self and total milliseconds, with functionsCounted, scriptMs (the self time of them all) and coveredMs. The runtime addon's own functions, gdharness answering its tools inside the game, are not among them: their self time is runtimeAddonMs, over runtimeAddonFunctions functions. complete is true when the engine's own totals arrived, which it sends only when the profiler is switched off: the runtime addon does that as a game quits, so a scene that does its work in _ready and quits is covered whole; otherwise the answer is the frames summed so far and the note says what that misses. frames counts the frames of the profile that arrived, and the engine's totals also hold the work after the last of them, so once complete a function run every frame can show a call more than frames. Self time counts a call into the engine's own methods as the caller's, and a call through super too, while the overridden function's row counts it again: such a row carries selfIncludesSuper, so self times along an override chain overlap. The profiler's own cost of timing a call lands in the self time of the function called; with the runtime addon it is measured each run on an empty function, as perCallOverheadUs, and a row with at least half its selfMs in it carries overheadMs, since a small function called often costs that much less without the profiler. A function the profiler started inside is left out, since its time would be counted from the engine's start. The milliseconds are time passed while the function ran, not processor time given to it, so they grow when other processes compete for the processor: compare two profiles taken under the same load. They are also summed over every thread: a function run by WorkerThreadPool or a Thread on several threads at once counts each thread's time, the engine does not say which thread a call ran on, and a function waiting for them has the wait in its own self time, so selfMs is not time taken from frames, and scriptMs can exceed coveredMs, which the note then says. A property's get and set blocks are named Class.@name_getter and Class.@name_setter, as the engine names them, and their rows carry property, the property's name. A game the editor plays that was not started with profile: true says, in the answer, that it was profiled from the moment it was asked. A profile with mark answers marked: true; one with since answers since: \"mark\" and markSetMsAgo, and lessThanAtMark counts the functions left out for reading less than at the mark, which the engine's own totals, capped, can do when they replace the frames' sum.",
|
|
12425
13048
|
defaultOperation: "start"
|
|
12426
13049
|
},
|
|
12427
13050
|
{
|
|
@@ -12589,7 +13212,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12589
13212
|
property: {
|
|
12590
13213
|
type: "string",
|
|
12591
13214
|
ops: ["property", "find"],
|
|
12592
|
-
description:
|
|
13215
|
+
description: `property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A key holding a colon, a bracket or a quote is written in double quotes, 'stock:"base:dew"', as a refusal listing the keys writes it. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A vector, a colour, a rectangle or a transform is stepped into by its components as GDScript names them, so one axis is one number: "size:x", "global_position:y", "modulate:a", "global_transform:origin:z"; a name the value does not have is refused, naming the ones it has. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. An engine method that reads takes literal arguments in its brackets, words in double quotes, numbers, true, false or null, so a control's theme is read where it is drawn: 'get_theme_stylebox("panel"):content_margin_top' or 'get_theme_constant("separation")'. A game method is given none in a path, since nothing marks one as only reading; runtime_invoke call takes those. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.`
|
|
12593
13216
|
},
|
|
12594
13217
|
depth: {
|
|
12595
13218
|
type: "integer",
|
|
@@ -12809,7 +13432,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12809
13432
|
name: "runtime_input",
|
|
12810
13433
|
title: "Send input to the game",
|
|
12811
13434
|
description: "Input to the running game: a whole click on a Control or a 3D node named by path or on a control named by the words on it, an item chosen out of a menu, typing into whatever has the focus, or a raw action, key, mouse button, mouse motion or drag. All of it works headless, where the viewport is the project's window size and the GUI only takes what is inside it. On gdharness's own desktop, where a windowed run goes, Windows refuses to say where the pointer is: every pointer answer there carries pointer_on_hidden_desktop, and the first of the run pointer_note saying why it matters: get_mouse_position() on the root window, and the mouse positions derived from it, read a meaningless position, while what reads the event itself (the control under it, hover, tooltips, input handlers) gets the injected position. A native window no real pointer has entered gets no hover from Godot, so the runtime supplies it there and the answers say hover_supplied.",
|
|
12812
|
-
answer:
|
|
13435
|
+
answer: `click: what was under the pointer, and what became of the control: in_tree, removed or freed. Named by says, found says which match it was, its index being the index argument that presses the same match again, found.item names the tab, list entry, tree row or menu bar title pressed (kind; index, the item's own number in its control, which is not found.index; says; and for a row the column of the cell saying the words), and found.pressedOn says "the words" for words in a RichTextLabel pressed where they are drawn, or "the middle of the label" with a note saying why they could not be placed; on a rich label link_pressed says whether the press reached a link, with link_meta giving the meta the label reports clicked, an empty one included. A control scrolled into view first is scrolled each container out from it a frame after the one inside, and scrolled_into_view says whether a scroll value changed. A button in toggle mode, a CheckBox or CheckButton included, answers pressed with before and after, its button_pressed either side of the click: the same value twice is a click the game undid, and after is null when the click freed it. A refusal for a pointer turned off names where mouse_behavior_recursive was set. On a 3D node landed says the interface did not swallow the press: no control under the pointer stopped it, a control letting the pointer pass included, and every viewport on the way out from the room had the pointer over what shows the room. choose: what was chosen, read before the press, which is heard rather than assumed; what became of the control named (in_tree, removed or freed, since a game can rebuild its screen on the pick); and what the button in front of the menu shows now when it is still there. A menu is opened rather than clicked because its items are drawn rather than built, and the item is looked for as the menu stands once open, since a game can fill a menu as it opens. action, key, mouse_click and joypad_button: the event as it was sent, with whole saying whether the press was let go of again, for a key the keycode, physical_keycode and unicode it carried, and for a controller button its number, its name and the device. mouse_motion: the position and the relative movement sent. mouse_drag: from, to, motions sent and relative_total. joypad_motion: the axis by number and name, the value and the device. None of these is aimed at anything, so none says what the event reached: click on a nodePath or says answers what was under the pointer.`,
|
|
12813
13436
|
parameters: {
|
|
12814
13437
|
projectPath: RUNNING_PROJECT_PATH,
|
|
12815
13438
|
pid: RUNNING_PID,
|
|
@@ -12821,7 +13444,7 @@ var init_tool_definitions = __esm(() => {
|
|
|
12821
13444
|
says: {
|
|
12822
13445
|
type: "string",
|
|
12823
13446
|
ops: ["click"],
|
|
12824
|
-
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one. A match saying the words only as part of more is named under found.partOf, and is refused when another control says exactly the words (hidden, off the screen, covered, or on screen taking clicks without being a button), since it would stand in for that one, and when it lets the pointer through to what is under it; index presses it anyway. Words found only inside longer words (\"out\" in \"Scout\") rank after every other match and are refused unless index is given; words starting a longer word (\"Recruit\" in \"Recruits\") count. An empty field says its placeholder, and a control with no text of its own, a picture on a button, says its accessibility name. An item of an open menu is drawn by the menu rather than held as a control, so a click finds none, and the refusal names the menu and the choose call that picks it."
|
|
13447
|
+
description: "click: the words on the control to click, matched the way runtime_inspect find matches says (a plain word is contained in the text, a pattern with * or ? is a glob over it, and | separates alternatives), among the controls on screen, and pressed in the same frame it was found in, so a panel rebuilding between a find and a click cannot free it first. Words on a label inside a button press the button. A control with something drawn over its centre that would take the pointer, such as a screen shown over it or a dialog stacked above its own, is not on screen; nor is one on a hidden canvas layer, one outside the viewport, or one a container clips away that a click does not scroll. The refusal counts those apart. The matches are ranked the way a person picks what to press: an enabled button before anything else, a disabled one included, then a control whose whole text, or one whole line of it such as the title on a card, is the words before one saying them as part of more, and the best one is pressed; found.picked says why. Several matches at the best rank are refused with each one listed, and index picks one. A match saying the words only as part of more is named under found.partOf, and is refused when another control says exactly the words (hidden, off the screen, covered, or on screen taking clicks without being a button), since it would stand in for that one, and when it lets the pointer through to what is under it; index presses it anyway. Words found only inside longer words (\"out\" in \"Scout\") rank after every other match and are refused unless index is given; words starting a longer word (\"Recruit\" in \"Recruits\") count. An empty field says its placeholder, and a control with no text of its own, a picture on a button, says its accessibility name. A tab, a list entry, a tree row and a menu bar title each count as a match of their own and are pressed where they are drawn, after their control scrolls them into view; a disabled one is refused. An item of an open menu is drawn by the menu rather than held as a control, so a click finds none, and the refusal names the menu and the choose call that picks it."
|
|
12825
13448
|
},
|
|
12826
13449
|
action: {
|
|
12827
13450
|
type: "string",
|
|
@@ -34901,12 +35524,12 @@ var init_stdio = __esm(() => {
|
|
|
34901
35524
|
|
|
34902
35525
|
// src/autoload-failure.ts
|
|
34903
35526
|
import { existsSync as existsSync15, readFileSync as readFileSync13 } from "node:fs";
|
|
34904
|
-
import { join as
|
|
35527
|
+
import { join as join17 } from "node:path";
|
|
34905
35528
|
function scriptsAutoloaded(projectPath) {
|
|
34906
35529
|
const scripts = new Map;
|
|
34907
35530
|
let settings;
|
|
34908
35531
|
try {
|
|
34909
|
-
settings = parseProjectGodot(readFileSync13(
|
|
35532
|
+
settings = parseProjectGodot(readFileSync13(join17(projectPath, "project.godot"), "utf8"));
|
|
34910
35533
|
} catch {
|
|
34911
35534
|
return scripts;
|
|
34912
35535
|
}
|
|
@@ -34915,7 +35538,7 @@ function scriptsAutoloaded(projectPath) {
|
|
|
34915
35538
|
scripts.set(`res://${path}`, name);
|
|
34916
35539
|
continue;
|
|
34917
35540
|
}
|
|
34918
|
-
const scene =
|
|
35541
|
+
const scene = join17(projectPath, path);
|
|
34919
35542
|
if (!existsSync15(scene)) {
|
|
34920
35543
|
continue;
|
|
34921
35544
|
}
|
|
@@ -34928,22 +35551,21 @@ function scriptsAutoloaded(projectPath) {
|
|
|
34928
35551
|
return scripts;
|
|
34929
35552
|
}
|
|
34930
35553
|
function autoloadNotCompiled(entry, autoloaded) {
|
|
34931
|
-
const path =
|
|
35554
|
+
const path = notCompiledIn(entry);
|
|
34932
35555
|
const name = path === undefined ? undefined : autoloaded.get(path);
|
|
34933
35556
|
return path === undefined || name === undefined ? null : { name, path };
|
|
34934
35557
|
}
|
|
34935
|
-
var NOT_COMPILED;
|
|
34936
35558
|
var init_autoload_failure = __esm(() => {
|
|
34937
35559
|
init_resources();
|
|
35560
|
+
init_script_origins();
|
|
34938
35561
|
init_setup();
|
|
34939
|
-
NOT_COMPILED = /^Failed to load script "(res:\/\/[^"]+)" with error "Compilation failed"/;
|
|
34940
35562
|
});
|
|
34941
35563
|
|
|
34942
35564
|
// src/runtime-client.ts
|
|
34943
35565
|
import { existsSync as existsSync16, readdirSync as readdirSync10, readFileSync as readFileSync14, statSync as statSync8, unlinkSync as unlinkSync2 } from "node:fs";
|
|
34944
35566
|
import { createConnection } from "node:net";
|
|
34945
35567
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
34946
|
-
import { dirname as dirname10, join as
|
|
35568
|
+
import { dirname as dirname10, join as join18, resolve as resolve4 } from "node:path";
|
|
34947
35569
|
import { setTimeout as delay } from "node:timers/promises";
|
|
34948
35570
|
function runtimeDirectory(variables = process.env) {
|
|
34949
35571
|
const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
|
|
@@ -34951,19 +35573,19 @@ function runtimeDirectory(variables = process.env) {
|
|
|
34951
35573
|
return explicit;
|
|
34952
35574
|
}
|
|
34953
35575
|
const perUser = envValue("XDG_RUNTIME_DIR", variables);
|
|
34954
|
-
return
|
|
35576
|
+
return join18(perUser ?? tmpdir4(), "gdharness");
|
|
34955
35577
|
}
|
|
34956
35578
|
function runtimeDirectories(variables = process.env) {
|
|
34957
35579
|
const candidates = [runtimeDirectory(variables)];
|
|
34958
35580
|
const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
|
|
34959
35581
|
for (const base of fallbacks) {
|
|
34960
|
-
candidates.push(
|
|
35582
|
+
candidates.push(join18(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
|
|
34961
35583
|
}
|
|
34962
35584
|
return [...new Set(candidates.map((path) => resolve4(path)))];
|
|
34963
35585
|
}
|
|
34964
35586
|
function errorReportOf(pid, directories = runtimeDirectories()) {
|
|
34965
35587
|
for (const directory of directories) {
|
|
34966
|
-
const path =
|
|
35588
|
+
const path = join18(directory, `runtime-${pid}.log`);
|
|
34967
35589
|
if (existsSync16(path)) {
|
|
34968
35590
|
return path;
|
|
34969
35591
|
}
|
|
@@ -35113,14 +35735,14 @@ function announcedIn(directory) {
|
|
|
35113
35735
|
for (const entry of readdirSync10(directory)) {
|
|
35114
35736
|
const report = ERROR_REPORT_PATTERN.exec(entry);
|
|
35115
35737
|
if (report) {
|
|
35116
|
-
sweepErrorReport(
|
|
35738
|
+
sweepErrorReport(join18(directory, entry), Number.parseInt(report[1] ?? "", 10));
|
|
35117
35739
|
continue;
|
|
35118
35740
|
}
|
|
35119
35741
|
const match = ANNOUNCEMENT_PATTERN.exec(entry);
|
|
35120
35742
|
if (!match) {
|
|
35121
35743
|
continue;
|
|
35122
35744
|
}
|
|
35123
|
-
const file =
|
|
35745
|
+
const file = join18(directory, entry);
|
|
35124
35746
|
const pid = Number.parseInt(match[1] ?? "", 10);
|
|
35125
35747
|
const running = alive(pid);
|
|
35126
35748
|
entries.push({ file, pid, running });
|
|
@@ -35148,7 +35770,7 @@ function announcedIn(directory) {
|
|
|
35148
35770
|
found.push(announced);
|
|
35149
35771
|
}
|
|
35150
35772
|
const held = new Set(entries.map((one) => one.file));
|
|
35151
|
-
const here =
|
|
35773
|
+
const here = join18(directory, ".");
|
|
35152
35774
|
for (const file of verdicts.keys()) {
|
|
35153
35775
|
if (dirname10(file) === here && !held.has(file)) {
|
|
35154
35776
|
verdicts.delete(file);
|
|
@@ -35157,7 +35779,7 @@ function announcedIn(directory) {
|
|
|
35157
35779
|
return found;
|
|
35158
35780
|
}
|
|
35159
35781
|
async function announcementEnded(pid, directories = runtimeDirectories()) {
|
|
35160
|
-
const files = directories.map((directory) =>
|
|
35782
|
+
const files = directories.map((directory) => join18(directory, `runtime-${pid}.json`)).filter((file) => existsSync16(file));
|
|
35161
35783
|
const first = files[0];
|
|
35162
35784
|
if (first === undefined) {
|
|
35163
35785
|
return;
|
|
@@ -35569,9 +36191,9 @@ var init_runtime_client = __esm(() => {
|
|
|
35569
36191
|
|
|
35570
36192
|
// src/boot-note.ts
|
|
35571
36193
|
import { mkdirSync as mkdirSync6, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
|
|
35572
|
-
import { dirname as dirname11, join as
|
|
36194
|
+
import { dirname as dirname11, join as join19 } from "node:path";
|
|
35573
36195
|
function bootNotePath(projectPath) {
|
|
35574
|
-
return
|
|
36196
|
+
return join19(projectPath, ".godot", "gdharness-boot.json");
|
|
35575
36197
|
}
|
|
35576
36198
|
function readScenes(projectPath) {
|
|
35577
36199
|
try {
|
|
@@ -35618,9 +36240,9 @@ var init_boot_note = __esm(() => {
|
|
|
35618
36240
|
|
|
35619
36241
|
// src/breakpoint-note.ts
|
|
35620
36242
|
import { mkdirSync as mkdirSync7, readFileSync as readFileSync16, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
|
|
35621
|
-
import { dirname as dirname12, isAbsolute as isAbsolute4, join as
|
|
36243
|
+
import { dirname as dirname12, isAbsolute as isAbsolute4, join as join20, relative as relative3 } from "node:path";
|
|
35622
36244
|
function breakpointNotePath(projectPath) {
|
|
35623
|
-
return
|
|
36245
|
+
return join20(projectPath, ".godot", "gdharness-breakpoints.json");
|
|
35624
36246
|
}
|
|
35625
36247
|
function writeBreakpointNote(projectPath, held) {
|
|
35626
36248
|
const path = breakpointNotePath(projectPath);
|
|
@@ -35661,7 +36283,7 @@ function readBreakpointNote(projectPath) {
|
|
|
35661
36283
|
}
|
|
35662
36284
|
const lines = Array.isArray(listed) ? listed.filter((line) => Number.isInteger(line) && line > 0) : [];
|
|
35663
36285
|
if (lines.length > 0) {
|
|
35664
|
-
held.push({ scriptPath: realPathOr(
|
|
36286
|
+
held.push({ scriptPath: realPathOr(join20(projectPath, script)), lines });
|
|
35665
36287
|
}
|
|
35666
36288
|
}
|
|
35667
36289
|
return held;
|
|
@@ -35711,9 +36333,9 @@ var init_whole_file = __esm(() => {
|
|
|
35711
36333
|
|
|
35712
36334
|
// src/class-note.ts
|
|
35713
36335
|
import { mkdirSync as mkdirSync8, readFileSync as readFileSync17 } from "node:fs";
|
|
35714
|
-
import { dirname as dirname13, join as
|
|
36336
|
+
import { dirname as dirname13, join as join21 } from "node:path";
|
|
35715
36337
|
function classNotePath(projectPath) {
|
|
35716
|
-
return
|
|
36338
|
+
return join21(projectPath, ".godot", "gdharness-classes.json");
|
|
35717
36339
|
}
|
|
35718
36340
|
function readClassNote(projectPath) {
|
|
35719
36341
|
try {
|
|
@@ -35760,9 +36382,9 @@ var init_class_note = __esm(() => {
|
|
|
35760
36382
|
|
|
35761
36383
|
// src/config-pin.ts
|
|
35762
36384
|
import { existsSync as existsSync17, readFileSync as readFileSync18 } from "node:fs";
|
|
35763
|
-
import { join as
|
|
36385
|
+
import { join as join22 } from "node:path";
|
|
35764
36386
|
function versionsNamedInConfig(projectPath) {
|
|
35765
|
-
const config =
|
|
36387
|
+
const config = join22(projectPath, ".mcp.json");
|
|
35766
36388
|
if (!existsSync17(config)) {
|
|
35767
36389
|
return [];
|
|
35768
36390
|
}
|
|
@@ -35790,9 +36412,9 @@ var init_config_pin = __esm(() => {
|
|
|
35790
36412
|
|
|
35791
36413
|
// src/cut-test-run.ts
|
|
35792
36414
|
import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync19, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "node:fs";
|
|
35793
|
-
import { dirname as dirname14, join as
|
|
36415
|
+
import { dirname as dirname14, join as join23 } from "node:path";
|
|
35794
36416
|
function keptAt(projectPath) {
|
|
35795
|
-
return
|
|
36417
|
+
return join23(projectPath, ".godot", "gdharness-reports", "cut-run.json");
|
|
35796
36418
|
}
|
|
35797
36419
|
function keepCutTestRun(projectPath, run) {
|
|
35798
36420
|
const path = keptAt(projectPath);
|
|
@@ -36631,10 +37253,10 @@ import {
|
|
|
36631
37253
|
writeSync
|
|
36632
37254
|
} from "node:fs";
|
|
36633
37255
|
import { tmpdir as tmpdir5 } from "node:os";
|
|
36634
|
-
import { join as
|
|
37256
|
+
import { join as join24 } from "node:path";
|
|
36635
37257
|
import { setTimeout as delay3 } from "node:timers/promises";
|
|
36636
37258
|
function helperAssembly(directory = tmpdir5()) {
|
|
36637
|
-
return
|
|
37259
|
+
return join24(directory, `gdharness-desktop-${DIGEST}.dll`);
|
|
36638
37260
|
}
|
|
36639
37261
|
async function ensureHelperCompiled(directory = tmpdir5(), budgetMs = HELPER_COMPILE_MS, wait = true) {
|
|
36640
37262
|
const assembly = helperAssembly(directory);
|
|
@@ -36674,8 +37296,8 @@ async function ensureHelperCompiled(directory = tmpdir5(), budgetMs = HELPER_COM
|
|
|
36674
37296
|
return `the desktop helper was not compiled within ${String(budgetMs)} ms`;
|
|
36675
37297
|
}
|
|
36676
37298
|
async function compileHelper(directory, assembly, budgetMs) {
|
|
36677
|
-
const scratch = mkdtempSync2(
|
|
36678
|
-
const source =
|
|
37299
|
+
const scratch = mkdtempSync2(join24(directory, "gdharness-desktop-source-"));
|
|
37300
|
+
const source = join24(scratch, "helper.cs");
|
|
36679
37301
|
writeFileSync12(source, SOURCE, "utf8");
|
|
36680
37302
|
const literal = (path) => `'${path.replaceAll("'", "''")}'`;
|
|
36681
37303
|
const script = [
|
|
@@ -36889,12 +37511,12 @@ import {
|
|
|
36889
37511
|
statSync as statSync10,
|
|
36890
37512
|
writeFileSync as writeFileSync13
|
|
36891
37513
|
} from "node:fs";
|
|
36892
|
-
import { dirname as dirname15, join as
|
|
37514
|
+
import { dirname as dirname15, join as join25 } from "node:path";
|
|
36893
37515
|
function editorLogPath(projectPath) {
|
|
36894
|
-
return
|
|
37516
|
+
return join25(projectPath, ".godot", "gdharness-editor.log");
|
|
36895
37517
|
}
|
|
36896
37518
|
function editorLogNotePath(projectPath) {
|
|
36897
|
-
return
|
|
37519
|
+
return join25(projectPath, ".godot", "gdharness-editor.json");
|
|
36898
37520
|
}
|
|
36899
37521
|
function readEditorLogNote(projectPath) {
|
|
36900
37522
|
try {
|
|
@@ -37930,7 +38552,7 @@ var init_rename = __esm(() => {
|
|
|
37930
38552
|
import { existsSync as existsSync22, realpathSync as realpathSync4 } from "node:fs";
|
|
37931
38553
|
import { readFile, realpath } from "node:fs/promises";
|
|
37932
38554
|
import { createConnection as createConnection3 } from "node:net";
|
|
37933
|
-
import { dirname as dirname17, join as
|
|
38555
|
+
import { dirname as dirname17, join as join26, resolve as resolve5 } from "node:path";
|
|
37934
38556
|
import { setTimeout as delay4 } from "node:timers/promises";
|
|
37935
38557
|
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
|
|
37936
38558
|
function initializeTimeoutMs() {
|
|
@@ -38608,7 +39230,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
|
|
|
38608
39230
|
} catch {
|
|
38609
39231
|
throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
|
|
38610
39232
|
}
|
|
38611
|
-
if (!existsSync22(
|
|
39233
|
+
if (!existsSync22(join26(projectPath, "project.godot"))) {
|
|
38612
39234
|
throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
|
|
38613
39235
|
}
|
|
38614
39236
|
const contained = resolveWithinProject(projectPath, scriptPathValue);
|
|
@@ -38769,7 +39391,7 @@ var init_lsp_client = __esm(() => {
|
|
|
38769
39391
|
|
|
38770
39392
|
// src/rename-symbol.ts
|
|
38771
39393
|
import { existsSync as existsSync23, mkdirSync as mkdirSync12, readdirSync as readdirSync11, realpathSync as realpathSync5, rmSync as rmSync12, writeFileSync as writeFileSync15 } from "node:fs";
|
|
38772
|
-
import { basename as basename7, dirname as dirname18, join as
|
|
39394
|
+
import { basename as basename7, dirname as dirname18, join as join27, relative as relative4, resolve as resolve6 } from "node:path";
|
|
38773
39395
|
function resourcePathOf(projectPath, absolute) {
|
|
38774
39396
|
const inside = relative4(onDiskSpelling(projectPath), onDiskSpelling(absolute));
|
|
38775
39397
|
if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
|
|
@@ -38782,7 +39404,7 @@ function onDiskSpelling(path) {
|
|
|
38782
39404
|
let at = resolve6(path);
|
|
38783
39405
|
for (;; ) {
|
|
38784
39406
|
try {
|
|
38785
|
-
return
|
|
39407
|
+
return join27(realpathSync5.native(at), ...missing.reverse());
|
|
38786
39408
|
} catch {
|
|
38787
39409
|
const parent = dirname18(at);
|
|
38788
39410
|
if (parent === at) {
|
|
@@ -39210,15 +39832,15 @@ ${mention.kind}`;
|
|
|
39210
39832
|
};
|
|
39211
39833
|
}
|
|
39212
39834
|
function writeLeftAloneDetail(projectPath, symbol, detail) {
|
|
39213
|
-
const directory =
|
|
39835
|
+
const directory = join27(projectPath, ".godot", "gdharness-renames");
|
|
39214
39836
|
mkdirSync12(directory, { recursive: true });
|
|
39215
39837
|
detailWrites += 1;
|
|
39216
|
-
const path =
|
|
39838
|
+
const path = join27(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
|
|
39217
39839
|
writeFileSync15(path, `${JSON.stringify(detail, null, 2)}
|
|
39218
39840
|
`, "utf8");
|
|
39219
39841
|
const kept = readdirSync11(directory).filter((name) => name.endsWith(".json")).sort().reverse();
|
|
39220
39842
|
for (const old of kept.slice(10)) {
|
|
39221
|
-
rmSync12(
|
|
39843
|
+
rmSync12(join27(directory, old), { force: true });
|
|
39222
39844
|
}
|
|
39223
39845
|
return path;
|
|
39224
39846
|
}
|
|
@@ -39267,10 +39889,10 @@ import {
|
|
|
39267
39889
|
statSync as statSync11,
|
|
39268
39890
|
writeFileSync as writeFileSync16
|
|
39269
39891
|
} from "node:fs";
|
|
39270
|
-
import { basename as basename8, dirname as dirname19, extname as extname3, join as
|
|
39892
|
+
import { basename as basename8, dirname as dirname19, extname as extname3, join as join28, relative as relative5 } from "node:path";
|
|
39271
39893
|
function filesUnder(directory) {
|
|
39272
39894
|
return readdirSync12(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
39273
|
-
const path =
|
|
39895
|
+
const path = join28(directory, entry.name);
|
|
39274
39896
|
return entry.isDirectory() ? filesUnder(path) : [path];
|
|
39275
39897
|
});
|
|
39276
39898
|
}
|
|
@@ -39291,8 +39913,8 @@ function unseenByTheEngine(projectPath, path) {
|
|
|
39291
39913
|
return `${path} is under ${step}, and the engine passes over every directory whose name starts with a dot`;
|
|
39292
39914
|
}
|
|
39293
39915
|
if (at < steps.length - 1) {
|
|
39294
|
-
directory =
|
|
39295
|
-
if (existsSync24(
|
|
39916
|
+
directory = join28(directory, step);
|
|
39917
|
+
if (existsSync24(join28(directory, ".gdignore"))) {
|
|
39296
39918
|
return `${path} is under a directory holding a .gdignore, which the engine does not load`;
|
|
39297
39919
|
}
|
|
39298
39920
|
}
|
|
@@ -39304,12 +39926,12 @@ function installedAddonOf(projectPath, path) {
|
|
|
39304
39926
|
if (steps[0] !== "addons" || steps[1] === undefined) {
|
|
39305
39927
|
return null;
|
|
39306
39928
|
}
|
|
39307
|
-
return existsSync24(
|
|
39929
|
+
return existsSync24(join28(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
|
|
39308
39930
|
}
|
|
39309
39931
|
function binaryResources(projectPath) {
|
|
39310
39932
|
const found = [];
|
|
39311
39933
|
const visit = (directory, prefix) => {
|
|
39312
|
-
if (existsSync24(
|
|
39934
|
+
if (existsSync24(join28(directory, ".gdignore"))) {
|
|
39313
39935
|
return;
|
|
39314
39936
|
}
|
|
39315
39937
|
for (const entry of readdirSync12(directory, { withFileTypes: true })) {
|
|
@@ -39318,7 +39940,7 @@ function binaryResources(projectPath) {
|
|
|
39318
39940
|
}
|
|
39319
39941
|
const path = `${prefix}${entry.name}`;
|
|
39320
39942
|
if (entry.isDirectory()) {
|
|
39321
|
-
visit(
|
|
39943
|
+
visit(join28(directory, entry.name), `${path}/`);
|
|
39322
39944
|
} else if (/\.(scn|res)$/i.test(entry.name)) {
|
|
39323
39945
|
found.push(path);
|
|
39324
39946
|
}
|
|
@@ -39328,7 +39950,7 @@ function binaryResources(projectPath) {
|
|
|
39328
39950
|
return found;
|
|
39329
39951
|
}
|
|
39330
39952
|
function targetPathOf(projectPath, absolute) {
|
|
39331
|
-
const inside = relative5(onDiskSpelling(projectPath),
|
|
39953
|
+
const inside = relative5(onDiskSpelling(projectPath), join28(onDiskSpelling(dirname19(absolute)), basename8(absolute)));
|
|
39332
39954
|
if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
|
|
39333
39955
|
return null;
|
|
39334
39956
|
}
|
|
@@ -39383,7 +40005,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
|
|
|
39383
40005
|
return { ok: false, reason: `${unseen}, so nothing there could load what was moved.` };
|
|
39384
40006
|
}
|
|
39385
40007
|
if (!isDirectory) {
|
|
39386
|
-
const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync24(
|
|
40008
|
+
const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync24(join28(projectPath, path.slice("res://".length))));
|
|
39387
40009
|
if (owner !== undefined && owner !== null) {
|
|
39388
40010
|
return { ok: false, reason: `${from} belongs to ${owner} and moves with it: move ${owner} instead.` };
|
|
39389
40011
|
}
|
|
@@ -39417,12 +40039,12 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
|
|
|
39417
40039
|
scripts,
|
|
39418
40040
|
moves,
|
|
39419
40041
|
named: [from],
|
|
39420
|
-
exists: (path) => existsSync24(
|
|
40042
|
+
exists: (path) => existsSync24(join28(projectPath, path.slice("res://".length)))
|
|
39421
40043
|
}, { edits: [], mentions: [] });
|
|
39422
40044
|
plan = mentionIgnored(plan, ignoredTexts(projectPath), null, [from]);
|
|
39423
40045
|
const binaries = [];
|
|
39424
40046
|
for (const path of binaryResources(projectPath)) {
|
|
39425
|
-
const bytes = readFileSync23(
|
|
40047
|
+
const bytes = readFileSync23(join28(projectPath, path.slice("res://".length)));
|
|
39426
40048
|
const names = [...moves.keys()].find((moving) => bytes.includes(moving));
|
|
39427
40049
|
if (names !== undefined) {
|
|
39428
40050
|
binaries.push({
|
|
@@ -39441,7 +40063,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
|
|
|
39441
40063
|
}));
|
|
39442
40064
|
const left = leftAloneReport(mentions);
|
|
39443
40065
|
const rewrites = rewritesOf(plan, texts, null);
|
|
39444
|
-
const absoluteOf = (path) =>
|
|
40066
|
+
const absoluteOf = (path) => join28(projectPath, path.slice("res://".length));
|
|
39445
40067
|
const rewritten = new Map(rewrites.map((rewrite) => [rewrite.path, rewrite]));
|
|
39446
40068
|
const moved = [...moves].map(([source, target]) => {
|
|
39447
40069
|
const rewrite = rewritten.get(source);
|
|
@@ -42882,742 +43504,243 @@ var init_godot_bridge = __esm(() => {
|
|
|
42882
43504
|
this.connectionInfo.greeted = true;
|
|
42883
43505
|
const pid = this.connectionInfo.editorPid;
|
|
42884
43506
|
if (!this.connectionInfo.openedByAServer && pid !== undefined && this.oneShotReader !== null) {
|
|
42885
|
-
this.identify(pid);
|
|
42886
|
-
return;
|
|
42887
|
-
}
|
|
42888
|
-
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
42889
|
-
}
|
|
42890
|
-
return;
|
|
42891
|
-
case "pong":
|
|
42892
|
-
if (this.connectionInfo) {
|
|
42893
|
-
this.connectionInfo.lastPongAt = new Date;
|
|
42894
|
-
}
|
|
42895
|
-
return;
|
|
42896
|
-
}
|
|
42897
|
-
}
|
|
42898
|
-
invokeToolDirect(toolName, args, counted, resourceKey) {
|
|
42899
|
-
if (!this.isConnected()) {
|
|
42900
|
-
return Promise.reject(new Error("Godot is not connected"));
|
|
42901
|
-
}
|
|
42902
|
-
const requestId = randomUUID();
|
|
42903
|
-
const message = {
|
|
42904
|
-
type: "tool_invoke",
|
|
42905
|
-
id: requestId,
|
|
42906
|
-
tool: toolName,
|
|
42907
|
-
args
|
|
42908
|
-
};
|
|
42909
|
-
return new Promise((resolve, reject) => {
|
|
42910
|
-
const timeout = setTimeout(() => {
|
|
42911
|
-
this.pendingRequests.delete(requestId);
|
|
42912
|
-
reject(new BridgeTimeout(toolName, this.timeoutMs));
|
|
42913
|
-
}, this.timeoutMs);
|
|
42914
|
-
this.pendingRequests.set(requestId, {
|
|
42915
|
-
toolName,
|
|
42916
|
-
timeout,
|
|
42917
|
-
resolve,
|
|
42918
|
-
reject,
|
|
42919
|
-
startedAt: Date.now(),
|
|
42920
|
-
resourceKey,
|
|
42921
|
-
counted
|
|
42922
|
-
});
|
|
42923
|
-
this.emitBridgeEvent("tool_start", {
|
|
42924
|
-
tool: toolName,
|
|
42925
|
-
id: requestId,
|
|
42926
|
-
args
|
|
42927
|
-
});
|
|
42928
|
-
try {
|
|
42929
|
-
this.sendMessage(message);
|
|
42930
|
-
} catch (error) {
|
|
42931
|
-
clearTimeout(timeout);
|
|
42932
|
-
this.pendingRequests.delete(requestId);
|
|
42933
|
-
reject(toError(error));
|
|
42934
|
-
}
|
|
42935
|
-
});
|
|
42936
|
-
}
|
|
42937
|
-
sendMessage(message) {
|
|
42938
|
-
if (this.socket?.readyState !== import_websocket.default.OPEN) {
|
|
42939
|
-
throw new Refusal("Godot is not connected");
|
|
42940
|
-
}
|
|
42941
|
-
this.socket.send(JSON.stringify(message));
|
|
42942
|
-
}
|
|
42943
|
-
startKeepalive() {
|
|
42944
|
-
this.stopKeepalive();
|
|
42945
|
-
this.pingInterval = setInterval(() => {
|
|
42946
|
-
if (!this.isConnected()) {
|
|
42947
|
-
return;
|
|
42948
|
-
}
|
|
42949
|
-
try {
|
|
42950
|
-
const ping = { type: "ping" };
|
|
42951
|
-
this.sendMessage(ping);
|
|
42952
|
-
} catch (error) {
|
|
42953
|
-
this.log("warn", `Failed to send ping: ${errorMessage(error)}`);
|
|
42954
|
-
}
|
|
42955
|
-
}, KEEPALIVE_INTERVAL_MS);
|
|
42956
|
-
}
|
|
42957
|
-
stopKeepalive() {
|
|
42958
|
-
if (!this.pingInterval) {
|
|
42959
|
-
return;
|
|
42960
|
-
}
|
|
42961
|
-
clearInterval(this.pingInterval);
|
|
42962
|
-
this.pingInterval = null;
|
|
42963
|
-
}
|
|
42964
|
-
belongsElsewhere(theirProject) {
|
|
42965
|
-
if (this.ownProject === null || theirProject.trim() === "") {
|
|
42966
|
-
return false;
|
|
42967
|
-
}
|
|
42968
|
-
return !isSameDirectory(this.ownProject, theirProject);
|
|
42969
|
-
}
|
|
42970
|
-
sendElsewhere(theirProject) {
|
|
42971
|
-
const said = `this server serves ${this.ownProject ?? ""}, and that editor has ${theirProject} open`;
|
|
42972
|
-
this.log("warn", `Turning away an editor from another project: ${said}`);
|
|
42973
|
-
const stranger = this.socket;
|
|
42974
|
-
this.socket = null;
|
|
42975
|
-
this.connectionInfo = null;
|
|
42976
|
-
this.stopKeepalive();
|
|
42977
|
-
const fits = closeReason(said, "start") === said;
|
|
42978
|
-
stranger?.close(OTHER_PROJECT_CLOSE_CODE, fits ? said : closeReason(`this server serves ${this.ownProject ?? ""}`, "end"));
|
|
42979
|
-
}
|
|
42980
|
-
elsewhereUnlessOneShot(theirProject, pid, openedByAServer) {
|
|
42981
|
-
const known = pid === undefined ? undefined : this.oneShots.get(pid);
|
|
42982
|
-
if (pid !== undefined && known !== undefined) {
|
|
42983
|
-
this.turnAway(pid, known);
|
|
42984
|
-
return;
|
|
42985
|
-
}
|
|
42986
|
-
const info = this.connectionInfo;
|
|
42987
|
-
const socket = this.socket;
|
|
42988
|
-
const reader = this.oneShotReader;
|
|
42989
|
-
if (pid === undefined || openedByAServer || info === null || reader === null) {
|
|
42990
|
-
this.sendElsewhere(theirProject);
|
|
42991
|
-
return;
|
|
42992
|
-
}
|
|
42993
|
-
info.identifying = true;
|
|
42994
|
-
reader(pid).catch(() => {
|
|
42995
|
-
return;
|
|
42996
|
-
}).then((option) => {
|
|
42997
|
-
if (this.socket !== socket || this.connectionInfo !== info) {
|
|
42998
|
-
return;
|
|
42999
|
-
}
|
|
43000
|
-
if (typeof option === "string") {
|
|
43001
|
-
this.oneShots.set(pid, option);
|
|
43002
|
-
this.turnAway(pid, option);
|
|
43003
|
-
return;
|
|
43004
|
-
}
|
|
43005
|
-
this.sendElsewhere(theirProject);
|
|
43006
|
-
});
|
|
43007
|
-
}
|
|
43008
|
-
identify(pid) {
|
|
43009
|
-
const known = this.oneShots.get(pid);
|
|
43010
|
-
if (known !== undefined) {
|
|
43011
|
-
this.turnAway(pid, known);
|
|
43012
|
-
return;
|
|
43013
|
-
}
|
|
43014
|
-
const info = this.connectionInfo;
|
|
43015
|
-
const socket = this.socket;
|
|
43016
|
-
const reader = this.oneShotReader;
|
|
43017
|
-
if (info === null || reader === null) {
|
|
43018
|
-
return;
|
|
43019
|
-
}
|
|
43020
|
-
info.identifying = true;
|
|
43021
|
-
reader(pid).catch(() => {
|
|
43022
|
-
return;
|
|
43023
|
-
}).then((option) => {
|
|
43024
|
-
if (this.socket !== socket || this.connectionInfo !== info) {
|
|
43025
|
-
return;
|
|
43026
|
-
}
|
|
43027
|
-
if (typeof option === "string") {
|
|
43028
|
-
this.oneShots.set(pid, option);
|
|
43029
|
-
this.turnAway(pid, option);
|
|
43030
|
-
return;
|
|
43031
|
-
}
|
|
43032
|
-
info.identifying = false;
|
|
43033
|
-
this.log("info", `Godot editor identified: pid ${pid}`);
|
|
43034
|
-
this.emitBridgeEvent("godot_connected", { projectPath: info.projectPath });
|
|
43035
|
-
});
|
|
43036
|
-
}
|
|
43037
|
-
turnAway(pid, option) {
|
|
43038
|
-
const said = `pid ${pid} was started with ${option}, which does one job and quits, so it is not an editor`;
|
|
43039
|
-
this.log("info", `Turning away an engine run: ${said}`);
|
|
43040
|
-
this.turnedAway = { pid, option, at: new Date };
|
|
43041
|
-
const run = this.socket;
|
|
43042
|
-
this.socket = null;
|
|
43043
|
-
this.connectionInfo = null;
|
|
43044
|
-
this.stopKeepalive();
|
|
43045
|
-
run?.close(ONE_SHOT_CLOSE_CODE, closeReason(said, "start"));
|
|
43046
|
-
}
|
|
43047
|
-
handleDisconnect(disconnectedSocket, reason) {
|
|
43048
|
-
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
43049
|
-
this.log("debug", "Ignoring stale Godot socket disconnect event");
|
|
43050
|
-
return;
|
|
43051
|
-
}
|
|
43052
|
-
this.stopKeepalive();
|
|
43053
|
-
this.socket = null;
|
|
43054
|
-
this.connectionInfo = null;
|
|
43055
|
-
this.emitBridgeEvent("godot_disconnected", {});
|
|
43056
|
-
this.rejectAllPending(reason);
|
|
43057
|
-
this.resourceQueues.clear();
|
|
43058
|
-
}
|
|
43059
|
-
emitBridgeEvent(eventName, payload) {
|
|
43060
|
-
this.emit(eventName, payload);
|
|
43061
|
-
}
|
|
43062
|
-
rejectAllPending(error) {
|
|
43063
|
-
for (const pending of this.pendingRequests.values()) {
|
|
43064
|
-
clearTimeout(pending.timeout);
|
|
43065
|
-
pending.reject(error);
|
|
43066
|
-
}
|
|
43067
|
-
this.pendingRequests.clear();
|
|
43068
|
-
}
|
|
43069
|
-
enqueueResourceRequest(resourceKey, task) {
|
|
43070
|
-
const previous = this.resourceQueues.get(resourceKey) ?? Promise.resolve();
|
|
43071
|
-
const taskPromise = previous.catch(() => {
|
|
43072
|
-
return;
|
|
43073
|
-
}).then(task);
|
|
43074
|
-
const tail = taskPromise.then(() => {
|
|
43075
|
-
return;
|
|
43076
|
-
}, () => {
|
|
43077
|
-
return;
|
|
43078
|
-
});
|
|
43079
|
-
this.resourceQueues.set(resourceKey, tail);
|
|
43080
|
-
return taskPromise.finally(() => {
|
|
43081
|
-
if (this.resourceQueues.get(resourceKey) === tail) {
|
|
43082
|
-
this.resourceQueues.delete(resourceKey);
|
|
43083
|
-
}
|
|
43084
|
-
});
|
|
43085
|
-
}
|
|
43086
|
-
getResourceKey(args) {
|
|
43087
|
-
const scenePath = this.getStringArg(args, "scenePath") ?? this.getStringArg(args, "scene_path");
|
|
43088
|
-
if (scenePath) {
|
|
43089
|
-
return `scene:${scenePath}`;
|
|
43090
|
-
}
|
|
43091
|
-
const resourcePath = this.getStringArg(args, "resourcePath") ?? this.getStringArg(args, "resource_path");
|
|
43092
|
-
if (resourcePath) {
|
|
43093
|
-
return `resource:${resourcePath}`;
|
|
43094
|
-
}
|
|
43095
|
-
return;
|
|
43096
|
-
}
|
|
43097
|
-
getStringArg(args, key) {
|
|
43098
|
-
const value = args[key];
|
|
43099
|
-
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
43100
|
-
}
|
|
43101
|
-
isIncomingMessage(value) {
|
|
43102
|
-
if (!value || typeof value !== "object") {
|
|
43103
|
-
return false;
|
|
43104
|
-
}
|
|
43105
|
-
const message = value;
|
|
43106
|
-
const type = message["type"];
|
|
43107
|
-
if (type !== "tool_result" && type !== "pong" && type !== "godot_ready") {
|
|
43108
|
-
return false;
|
|
43109
|
-
}
|
|
43110
|
-
if (type === "pong") {
|
|
43111
|
-
return true;
|
|
43112
|
-
}
|
|
43113
|
-
if (type === "godot_ready") {
|
|
43114
|
-
return typeof message["project_path"] === "string";
|
|
43115
|
-
}
|
|
43116
|
-
return typeof message["id"] === "string" && typeof message["success"] === "boolean" && (message["error"] === undefined || typeof message["error"] === "string") && (message["busy"] === undefined || typeof message["busy"] === "boolean");
|
|
43117
|
-
}
|
|
43118
|
-
log(level, message) {
|
|
43119
|
-
console.error(`[${new Date().toISOString()}] [GodotBridge:${level.toUpperCase()}] ${message}`);
|
|
43120
|
-
}
|
|
43121
|
-
};
|
|
43122
|
-
});
|
|
43123
|
-
|
|
43124
|
-
// src/junit.ts
|
|
43125
|
-
function decoded(text) {
|
|
43126
|
-
return text.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z]+);/g, (whole, entity) => {
|
|
43127
|
-
if (entity.startsWith("#")) {
|
|
43128
|
-
const code = entity.startsWith("#x") ? Number.parseInt(entity.slice(2), 16) : Number.parseInt(entity.slice(1), 10);
|
|
43129
|
-
if (!Number.isInteger(code) || code < 0 || code > 1114111) {
|
|
43130
|
-
throw new MalformedReportError(`Not a character: ${whole}`);
|
|
43131
|
-
}
|
|
43132
|
-
return String.fromCodePoint(code);
|
|
43133
|
-
}
|
|
43134
|
-
const named = ENTITIES[entity];
|
|
43135
|
-
if (named === undefined) {
|
|
43136
|
-
throw new MalformedReportError(`Unknown entity ${whole}`);
|
|
43137
|
-
}
|
|
43138
|
-
return named;
|
|
43139
|
-
});
|
|
43140
|
-
}
|
|
43141
|
-
function tagEnd(source, from) {
|
|
43142
|
-
let quote = null;
|
|
43143
|
-
for (let at = from + 1;at < source.length; at += 1) {
|
|
43144
|
-
const character = source[at];
|
|
43145
|
-
if (quote !== null) {
|
|
43146
|
-
if (character === quote) {
|
|
43147
|
-
quote = null;
|
|
43148
|
-
}
|
|
43149
|
-
} else if (character === '"' || character === "'") {
|
|
43150
|
-
quote = character;
|
|
43151
|
-
} else if (character === ">") {
|
|
43152
|
-
return at;
|
|
43153
|
-
}
|
|
43154
|
-
}
|
|
43155
|
-
return -1;
|
|
43156
|
-
}
|
|
43157
|
-
function parseXml(source) {
|
|
43158
|
-
const root = { name: "", attributes: {}, children: [], text: "" };
|
|
43159
|
-
const open = [root];
|
|
43160
|
-
let at = 0;
|
|
43161
|
-
const current = () => {
|
|
43162
|
-
const element = open.at(-1);
|
|
43163
|
-
if (element === undefined) {
|
|
43164
|
-
throw new MalformedReportError("A close tag with nothing open");
|
|
43165
|
-
}
|
|
43166
|
-
return element;
|
|
43167
|
-
};
|
|
43168
|
-
while (at < source.length) {
|
|
43169
|
-
const next = source.indexOf("<", at);
|
|
43170
|
-
if (next === -1) {
|
|
43171
|
-
current().text += decoded(source.slice(at));
|
|
43172
|
-
break;
|
|
43173
|
-
}
|
|
43174
|
-
current().text += decoded(source.slice(at, next));
|
|
43175
|
-
at = next;
|
|
43176
|
-
if (source.startsWith("<![CDATA[", at)) {
|
|
43177
|
-
const end = source.indexOf("]]>", at);
|
|
43178
|
-
if (end === -1) {
|
|
43179
|
-
throw new MalformedReportError("CDATA that never ends");
|
|
43180
|
-
}
|
|
43181
|
-
current().text += source.slice(at + "<![CDATA[".length, end);
|
|
43182
|
-
at = end + "]]>".length;
|
|
43183
|
-
continue;
|
|
43184
|
-
}
|
|
43185
|
-
if (source.startsWith("<!--", at)) {
|
|
43186
|
-
const end = source.indexOf("-->", at);
|
|
43187
|
-
if (end === -1) {
|
|
43188
|
-
throw new MalformedReportError("A comment that never ends");
|
|
43189
|
-
}
|
|
43190
|
-
at = end + "-->".length;
|
|
43191
|
-
continue;
|
|
43192
|
-
}
|
|
43193
|
-
if (source.startsWith("<?", at) || source.startsWith("<!", at)) {
|
|
43194
|
-
const end = source.indexOf(">", at);
|
|
43195
|
-
if (end === -1) {
|
|
43196
|
-
throw new MalformedReportError("A declaration that never ends");
|
|
43197
|
-
}
|
|
43198
|
-
at = end + 1;
|
|
43199
|
-
continue;
|
|
43200
|
-
}
|
|
43201
|
-
const end = tagEnd(source, at);
|
|
43202
|
-
if (end === -1) {
|
|
43203
|
-
throw new MalformedReportError("A tag that never ends");
|
|
43204
|
-
}
|
|
43205
|
-
const tag = source.slice(at + 1, end);
|
|
43206
|
-
at = end + 1;
|
|
43207
|
-
if (tag.startsWith("/")) {
|
|
43208
|
-
const closing = tag.slice(1).trim();
|
|
43209
|
-
const element = current();
|
|
43210
|
-
if (element === root || element.name !== closing) {
|
|
43211
|
-
throw new MalformedReportError(`</${closing}> closes <${element.name || "nothing"}>`);
|
|
43212
|
-
}
|
|
43213
|
-
open.pop();
|
|
43214
|
-
continue;
|
|
43215
|
-
}
|
|
43216
|
-
const selfClosing = tag.endsWith("/");
|
|
43217
|
-
const body = selfClosing ? tag.slice(0, -1) : tag;
|
|
43218
|
-
const nameMatch = /^([^\s/>]+)/.exec(body);
|
|
43219
|
-
if (!nameMatch?.[1]) {
|
|
43220
|
-
throw new MalformedReportError(`A tag with no name: <${tag}>`);
|
|
43221
|
-
}
|
|
43222
|
-
const attributes = {};
|
|
43223
|
-
let read = nameMatch[0].length;
|
|
43224
|
-
ATTRIBUTE.lastIndex = read;
|
|
43225
|
-
let attribute = ATTRIBUTE.exec(body);
|
|
43226
|
-
while (attribute) {
|
|
43227
|
-
attributes[attribute[1] ?? ""] = decoded(attribute[2] ?? attribute[3] ?? "");
|
|
43228
|
-
read = ATTRIBUTE.lastIndex;
|
|
43229
|
-
attribute = ATTRIBUTE.exec(body);
|
|
43230
|
-
}
|
|
43231
|
-
if (body.slice(read).trim() !== "") {
|
|
43232
|
-
throw new MalformedReportError(`Unreadable attributes in <${tag}>`);
|
|
43233
|
-
}
|
|
43234
|
-
const element = { name: nameMatch[1], attributes, children: [], text: "" };
|
|
43235
|
-
current().children.push(element);
|
|
43236
|
-
if (!selfClosing) {
|
|
43237
|
-
open.push(element);
|
|
43238
|
-
}
|
|
43239
|
-
}
|
|
43240
|
-
if (open.length !== 1) {
|
|
43241
|
-
throw new MalformedReportError(`<${current().name}> is never closed`);
|
|
43242
|
-
}
|
|
43243
|
-
return root;
|
|
43244
|
-
}
|
|
43245
|
-
function count(attributes, name) {
|
|
43246
|
-
const value = Number(attributes[name] ?? "0");
|
|
43247
|
-
return Number.isFinite(value) ? value : 0;
|
|
43248
|
-
}
|
|
43249
|
-
function ignoredYetRan(report, ignored) {
|
|
43250
|
-
const found = [];
|
|
43251
|
-
for (const entry of ignored) {
|
|
43252
|
-
const bare = entry.replace("res://", "");
|
|
43253
|
-
const at = bare.lastIndexOf(":");
|
|
43254
|
-
const suiteNamed = at === -1 ? null : bare.slice(0, at);
|
|
43255
|
-
const testNamed = at === -1 ? bare : bare.slice(at + 1);
|
|
43256
|
-
for (const suite of report.suites) {
|
|
43257
|
-
const suiteMatches = suiteNamed === null || suite.name === suiteNamed || (suite.path ?? "").includes(suiteNamed);
|
|
43258
|
-
const ran = suiteMatches ? suite.cases.find((one) => one.name === testNamed) : undefined;
|
|
43259
|
-
if (ran !== undefined) {
|
|
43260
|
-
found.push({ entry, ran: `${suite.name}:${ran.name}` });
|
|
43261
|
-
}
|
|
43262
|
-
}
|
|
43263
|
-
}
|
|
43264
|
-
return found;
|
|
43265
|
-
}
|
|
43266
|
-
function caseOf(suite, element) {
|
|
43267
|
-
const of = (kind) => element.children.filter((child) => child.name === kind).map((child) => {
|
|
43268
|
-
const detail = child.text.trim();
|
|
43269
|
-
return { kind, message: child.attributes["message"] ?? null, detail: detail === "" ? null : detail };
|
|
43270
|
-
});
|
|
43271
|
-
const reports = [...of("failure"), ...of("error"), ...of("skipped")];
|
|
43272
|
-
const status = reports.some((report) => report.kind === "error") ? "error" : reports.some((report) => report.kind === "failure") ? "failed" : reports.length > 0 ? "skipped" : "passed";
|
|
43273
|
-
return {
|
|
43274
|
-
suite: suite.attributes["name"] ?? "",
|
|
43275
|
-
name: element.attributes["name"] ?? "",
|
|
43276
|
-
status,
|
|
43277
|
-
time: count(element.attributes, "time"),
|
|
43278
|
-
message: reports[0]?.message ?? null,
|
|
43279
|
-
detail: reports[0]?.detail ?? null,
|
|
43280
|
-
reports
|
|
43281
|
-
};
|
|
43282
|
-
}
|
|
43283
|
-
function failedAnAssertion(entry) {
|
|
43284
|
-
return entry.reports.some((report) => report.kind === "failure");
|
|
43285
|
-
}
|
|
43286
|
-
function pathOf(suite) {
|
|
43287
|
-
const directory = suite.attributes["package"];
|
|
43288
|
-
const name = suite.attributes["name"];
|
|
43289
|
-
return directory && name ? `res://${directory}/${name}.gd` : null;
|
|
43290
|
-
}
|
|
43291
|
-
function parseJUnit(xml) {
|
|
43292
|
-
const document = parseXml(xml);
|
|
43293
|
-
const top = document.children.find((child) => child.name === "testsuites" || child.name === "testsuite");
|
|
43294
|
-
if (top === undefined) {
|
|
43295
|
-
throw new MalformedReportError("No <testsuites> or <testsuite> in the report");
|
|
43296
|
-
}
|
|
43297
|
-
const suiteElements = top.name === "testsuite" ? [top] : top.children.filter((child) => child.name === "testsuite");
|
|
43298
|
-
const suites = suiteElements.map((suite) => {
|
|
43299
|
-
const cases = suite.children.filter((child) => child.name === "testcase").map((element) => caseOf(suite, element));
|
|
43300
|
-
const failures = cases.filter(failedAnAssertion).length;
|
|
43301
|
-
const errors = cases.filter((entry) => entry.status === "error").length;
|
|
43302
|
-
const written = (kind) => suite.children.filter((child) => child.name === "testcase").reduce((sum, element) => sum + element.children.filter((child) => child.name === kind).length, 0);
|
|
43303
|
-
return {
|
|
43304
|
-
name: suite.attributes["name"] ?? "",
|
|
43305
|
-
path: pathOf(suite),
|
|
43306
|
-
tests: cases.length,
|
|
43307
|
-
discovered: count(suite.attributes, "tests"),
|
|
43308
|
-
failures,
|
|
43309
|
-
errors,
|
|
43310
|
-
hookFailures: Math.max(0, count(suite.attributes, "failures") - written("failure")),
|
|
43311
|
-
hookErrors: Math.max(0, count(suite.attributes, "errors") - written("error")),
|
|
43312
|
-
skipped: cases.filter((entry) => entry.status === "skipped").length,
|
|
43313
|
-
time: count(suite.attributes, "time"),
|
|
43314
|
-
cases
|
|
43315
|
-
};
|
|
43316
|
-
});
|
|
43317
|
-
const all = suites.flatMap((suite) => suite.cases);
|
|
43318
|
-
return {
|
|
43319
|
-
tests: all.length,
|
|
43320
|
-
failures: all.filter(failedAnAssertion).length,
|
|
43321
|
-
errors: all.filter((entry) => entry.status === "error").length,
|
|
43322
|
-
hookFailures: suites.reduce((sum, suite) => sum + suite.hookFailures, 0),
|
|
43323
|
-
hookErrors: suites.reduce((sum, suite) => sum + suite.hookErrors, 0),
|
|
43324
|
-
skipped: all.filter((entry) => entry.status === "skipped").length,
|
|
43325
|
-
time: suites.reduce((sum, suite) => sum + suite.time, 0),
|
|
43326
|
-
suites
|
|
43327
|
-
};
|
|
43328
|
-
}
|
|
43329
|
-
function hookFailuresPrinted(printed) {
|
|
43330
|
-
return printed.flatMap((line) => {
|
|
43331
|
-
const found = HOOK_FAILURE.exec(line);
|
|
43332
|
-
return found === null ? [] : [{ hook: found[2] ?? "", path: found[3] ?? "", line: Number(found[4]), message: found[1] ?? "" }];
|
|
43333
|
-
});
|
|
43334
|
-
}
|
|
43335
|
-
function whyNoReport(printed, asked) {
|
|
43336
|
-
const missing = printed.map((line) => NO_SUCH_PATH.exec(line)).find((found) => found !== null);
|
|
43337
|
-
if (missing !== undefined) {
|
|
43338
|
-
return `nothing at ${missing[1] ?? asked}`;
|
|
43339
|
-
}
|
|
43340
|
-
return printed.some((line) => NOTHING_RAN.test(line)) ? `no test cases found at ${asked}` : null;
|
|
43341
|
-
}
|
|
43342
|
-
function scriptErrorsPrinted(printed) {
|
|
43343
|
-
return discoveryListing(printed)?.errors ?? [];
|
|
43344
|
-
}
|
|
43345
|
-
function discoveryListing(printed) {
|
|
43346
|
-
const start = printed.findIndex((line) => line.includes(DISCOVERY_FAILED));
|
|
43347
|
-
if (start < 0) {
|
|
43348
|
-
return null;
|
|
43349
|
-
}
|
|
43350
|
-
const found = [];
|
|
43351
|
-
let message = null;
|
|
43352
|
-
let to = start + 1;
|
|
43353
|
-
for (const line of printed.slice(start + 1)) {
|
|
43354
|
-
const at = /^\s*at (res:\/\/.+):(\d+)\s*$/.exec(line);
|
|
43355
|
-
if (at !== null && message !== null) {
|
|
43356
|
-
found.push({ path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]), message });
|
|
43357
|
-
message = null;
|
|
43358
|
-
} else if (at === null && message === null && /^ {2}\S/.test(line)) {
|
|
43359
|
-
message = line.trim();
|
|
43360
|
-
} else {
|
|
43361
|
-
break;
|
|
43362
|
-
}
|
|
43363
|
-
to += 1;
|
|
43364
|
-
}
|
|
43365
|
-
const errors = found.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : (a.line ?? 0) - (b.line ?? 0));
|
|
43366
|
-
return { errors, from: start, to };
|
|
43367
|
-
}
|
|
43368
|
-
function unspelled(text) {
|
|
43369
|
-
return text.replace(/<(?:LF|CR|TAB|BS|FF|VT|BEL|ESC|DEL|0x[0-9A-F]{2})>/g, (spelling) => spelling.startsWith("<0x") ? String.fromCharCode(Number.parseInt(spelling.slice(3, 5), 16)) : SPELLED[spelling] ?? spelling);
|
|
43370
|
-
}
|
|
43371
|
-
function readDiff(printed) {
|
|
43372
|
-
let merged = "";
|
|
43373
|
-
let actual = "";
|
|
43374
|
-
let background = null;
|
|
43375
|
-
let last = 0;
|
|
43376
|
-
const take = (text) => {
|
|
43377
|
-
merged += text;
|
|
43378
|
-
if (background === null) {
|
|
43379
|
-
actual += text;
|
|
43380
|
-
} else if (background === "added") {
|
|
43381
|
-
actual += unspelled(text);
|
|
43382
|
-
}
|
|
43383
|
-
};
|
|
43384
|
-
for (const match of printed.matchAll(COLOUR2)) {
|
|
43385
|
-
take(printed.slice(last, match.index));
|
|
43386
|
-
last = match.index + match[0].length;
|
|
43387
|
-
const codes = (match[1] ?? "").split(";");
|
|
43388
|
-
if (codes[0] === "48" && codes[1] === "2") {
|
|
43389
|
-
background = Number(codes[2]) > Number(codes[3]) ? "missing" : "added";
|
|
43390
|
-
} else if (codes[0] === "0" || codes[0] === "") {
|
|
43391
|
-
background = null;
|
|
43392
|
-
}
|
|
43393
|
-
}
|
|
43394
|
-
take(printed.slice(last));
|
|
43395
|
-
return { merged, actual };
|
|
43396
|
-
}
|
|
43397
|
-
function withActualsPrinted(failed, printed) {
|
|
43398
|
-
const diffs = [...printed.replaceAll(`\r
|
|
43399
|
-
`, `
|
|
43400
|
-
`).matchAll(PRINTED_ACTUAL)].map((match) => ({
|
|
43401
|
-
suffix: match[3] ?? "",
|
|
43402
|
-
at: match[5] ?? "",
|
|
43403
|
-
...readDiff(match[1] ?? "")
|
|
43404
|
-
}));
|
|
43405
|
-
const used = new Set;
|
|
43406
|
-
return failed.map((entry) => {
|
|
43407
|
-
if (entry.detail === null) {
|
|
43408
|
-
return entry;
|
|
43507
|
+
this.identify(pid);
|
|
43508
|
+
return;
|
|
43509
|
+
}
|
|
43510
|
+
this.emitBridgeEvent("godot_connected", { projectPath: message.project_path });
|
|
43511
|
+
}
|
|
43512
|
+
return;
|
|
43513
|
+
case "pong":
|
|
43514
|
+
if (this.connectionInfo) {
|
|
43515
|
+
this.connectionInfo.lastPongAt = new Date;
|
|
43516
|
+
}
|
|
43517
|
+
return;
|
|
43518
|
+
}
|
|
43409
43519
|
}
|
|
43410
|
-
|
|
43411
|
-
|
|
43412
|
-
|
|
43413
|
-
if (used.has(index)) {
|
|
43414
|
-
continue;
|
|
43520
|
+
invokeToolDirect(toolName, args, counted, resourceKey) {
|
|
43521
|
+
if (!this.isConnected()) {
|
|
43522
|
+
return Promise.reject(new Error("Godot is not connected"));
|
|
43415
43523
|
}
|
|
43416
|
-
const
|
|
43417
|
-
|
|
43418
|
-
|
|
43419
|
-
|
|
43420
|
-
|
|
43421
|
-
|
|
43422
|
-
|
|
43423
|
-
|
|
43424
|
-
|
|
43524
|
+
const requestId = randomUUID();
|
|
43525
|
+
const message = {
|
|
43526
|
+
type: "tool_invoke",
|
|
43527
|
+
id: requestId,
|
|
43528
|
+
tool: toolName,
|
|
43529
|
+
args
|
|
43530
|
+
};
|
|
43531
|
+
return new Promise((resolve, reject) => {
|
|
43532
|
+
const timeout = setTimeout(() => {
|
|
43533
|
+
this.pendingRequests.delete(requestId);
|
|
43534
|
+
reject(new BridgeTimeout(toolName, this.timeoutMs));
|
|
43535
|
+
}, this.timeoutMs);
|
|
43536
|
+
this.pendingRequests.set(requestId, {
|
|
43537
|
+
toolName,
|
|
43538
|
+
timeout,
|
|
43539
|
+
resolve,
|
|
43540
|
+
reject,
|
|
43541
|
+
startedAt: Date.now(),
|
|
43542
|
+
resourceKey,
|
|
43543
|
+
counted
|
|
43544
|
+
});
|
|
43545
|
+
this.emitBridgeEvent("tool_start", {
|
|
43546
|
+
tool: toolName,
|
|
43547
|
+
id: requestId,
|
|
43548
|
+
args
|
|
43549
|
+
});
|
|
43550
|
+
try {
|
|
43551
|
+
this.sendMessage(message);
|
|
43552
|
+
} catch (error) {
|
|
43553
|
+
clearTimeout(timeout);
|
|
43554
|
+
this.pendingRequests.delete(requestId);
|
|
43555
|
+
reject(toError(error));
|
|
43556
|
+
}
|
|
43557
|
+
});
|
|
43558
|
+
}
|
|
43559
|
+
sendMessage(message) {
|
|
43560
|
+
if (this.socket?.readyState !== import_websocket.default.OPEN) {
|
|
43561
|
+
throw new Refusal("Godot is not connected");
|
|
43425
43562
|
}
|
|
43563
|
+
this.socket.send(JSON.stringify(message));
|
|
43426
43564
|
}
|
|
43427
|
-
|
|
43428
|
-
|
|
43429
|
-
|
|
43430
|
-
|
|
43431
|
-
|
|
43432
|
-
|
|
43565
|
+
startKeepalive() {
|
|
43566
|
+
this.stopKeepalive();
|
|
43567
|
+
this.pingInterval = setInterval(() => {
|
|
43568
|
+
if (!this.isConnected()) {
|
|
43569
|
+
return;
|
|
43570
|
+
}
|
|
43571
|
+
try {
|
|
43572
|
+
const ping = { type: "ping" };
|
|
43573
|
+
this.sendMessage(ping);
|
|
43574
|
+
} catch (error) {
|
|
43575
|
+
this.log("warn", `Failed to send ping: ${errorMessage(error)}`);
|
|
43576
|
+
}
|
|
43577
|
+
}, KEEPALIVE_INTERVAL_MS);
|
|
43433
43578
|
}
|
|
43434
|
-
|
|
43435
|
-
|
|
43436
|
-
|
|
43437
|
-
|
|
43579
|
+
stopKeepalive() {
|
|
43580
|
+
if (!this.pingInterval) {
|
|
43581
|
+
return;
|
|
43582
|
+
}
|
|
43583
|
+
clearInterval(this.pingInterval);
|
|
43584
|
+
this.pingInterval = null;
|
|
43438
43585
|
}
|
|
43439
|
-
|
|
43440
|
-
|
|
43441
|
-
|
|
43442
|
-
|
|
43443
|
-
|
|
43444
|
-
return detail;
|
|
43445
|
-
}
|
|
43446
|
-
return `${detail.slice(0, LONGEST_DETAIL)}
|
|
43447
|
-
[${detail.length - LONGEST_DETAIL} more characters left out]`;
|
|
43448
|
-
}
|
|
43449
|
-
function foldedReports(reports) {
|
|
43450
|
-
const first = new Map;
|
|
43451
|
-
for (const report of reports) {
|
|
43452
|
-
const key = JSON.stringify([report.kind, report.message, report.detail]);
|
|
43453
|
-
const seen = first.get(key);
|
|
43454
|
-
if (seen === undefined) {
|
|
43455
|
-
first.set(key, { report, times: 1 });
|
|
43456
|
-
} else {
|
|
43457
|
-
seen.times += 1;
|
|
43586
|
+
belongsElsewhere(theirProject) {
|
|
43587
|
+
if (this.ownProject === null || theirProject.trim() === "") {
|
|
43588
|
+
return false;
|
|
43589
|
+
}
|
|
43590
|
+
return !isSameDirectory(this.ownProject, theirProject);
|
|
43458
43591
|
}
|
|
43459
|
-
|
|
43460
|
-
|
|
43461
|
-
|
|
43462
|
-
|
|
43463
|
-
|
|
43464
|
-
|
|
43465
|
-
|
|
43466
|
-
|
|
43467
|
-
|
|
43468
|
-
let next = 0;
|
|
43469
|
-
return cases.map(({ entry, path }) => {
|
|
43470
|
-
const reports = read.slice(next, next + entry.reports.length);
|
|
43471
|
-
next += entry.reports.length;
|
|
43472
|
-
const folded = foldedReports(reports);
|
|
43473
|
-
const shown = folded.slice(0, MOST_REPORTS_SHOWN);
|
|
43474
|
-
return {
|
|
43475
|
-
suite: entry.suite,
|
|
43476
|
-
name: entry.name,
|
|
43477
|
-
status: entry.status,
|
|
43478
|
-
time: entry.time,
|
|
43479
|
-
path,
|
|
43480
|
-
message: reports[0]?.message ?? null,
|
|
43481
|
-
detail: shortened(reports[0]?.detail ?? null),
|
|
43482
|
-
...reports.length > 1 ? { reports: shown, reportCount: reports.length } : {},
|
|
43483
|
-
...folded.length > shown.length ? { reportsLeftOut: folded.length - shown.length } : {}
|
|
43484
|
-
};
|
|
43485
|
-
});
|
|
43486
|
-
}
|
|
43487
|
-
function failedWithin(failed, budget) {
|
|
43488
|
-
const sizes = failed.map((entry) => JSON.stringify(entry).length);
|
|
43489
|
-
const smallestFirst = sizes.map((_, index) => index).sort((a, b) => (sizes[a] ?? 0) - (sizes[b] ?? 0));
|
|
43490
|
-
const whole = new Set;
|
|
43491
|
-
let spent = 0;
|
|
43492
|
-
for (const index of smallestFirst) {
|
|
43493
|
-
const size = sizes[index] ?? 0;
|
|
43494
|
-
if (spent + size > budget) {
|
|
43495
|
-
break;
|
|
43592
|
+
sendElsewhere(theirProject) {
|
|
43593
|
+
const said = `this server serves ${this.ownProject ?? ""}, and that editor has ${theirProject} open`;
|
|
43594
|
+
this.log("warn", `Turning away an editor from another project: ${said}`);
|
|
43595
|
+
const stranger = this.socket;
|
|
43596
|
+
this.socket = null;
|
|
43597
|
+
this.connectionInfo = null;
|
|
43598
|
+
this.stopKeepalive();
|
|
43599
|
+
const fits = closeReason(said, "start") === said;
|
|
43600
|
+
stranger?.close(OTHER_PROJECT_CLOSE_CODE, fits ? said : closeReason(`this server serves ${this.ownProject ?? ""}`, "end"));
|
|
43496
43601
|
}
|
|
43497
|
-
|
|
43498
|
-
|
|
43499
|
-
|
|
43500
|
-
|
|
43501
|
-
|
|
43502
|
-
|
|
43602
|
+
elsewhereUnlessOneShot(theirProject, pid, openedByAServer) {
|
|
43603
|
+
const known = pid === undefined ? undefined : this.oneShots.get(pid);
|
|
43604
|
+
if (pid !== undefined && known !== undefined) {
|
|
43605
|
+
this.turnAway(pid, known);
|
|
43606
|
+
return;
|
|
43607
|
+
}
|
|
43608
|
+
const info = this.connectionInfo;
|
|
43609
|
+
const socket = this.socket;
|
|
43610
|
+
const reader = this.oneShotReader;
|
|
43611
|
+
if (pid === undefined || openedByAServer || info === null || reader === null) {
|
|
43612
|
+
this.sendElsewhere(theirProject);
|
|
43613
|
+
return;
|
|
43614
|
+
}
|
|
43615
|
+
info.identifying = true;
|
|
43616
|
+
reader(pid).catch(() => {
|
|
43617
|
+
return;
|
|
43618
|
+
}).then((option) => {
|
|
43619
|
+
if (this.socket !== socket || this.connectionInfo !== info) {
|
|
43620
|
+
return;
|
|
43621
|
+
}
|
|
43622
|
+
if (typeof option === "string") {
|
|
43623
|
+
this.oneShots.set(pid, option);
|
|
43624
|
+
this.turnAway(pid, option);
|
|
43625
|
+
return;
|
|
43626
|
+
}
|
|
43627
|
+
this.sendElsewhere(theirProject);
|
|
43628
|
+
});
|
|
43503
43629
|
}
|
|
43504
|
-
|
|
43505
|
-
|
|
43506
|
-
|
|
43507
|
-
|
|
43508
|
-
|
|
43509
|
-
|
|
43510
|
-
|
|
43511
|
-
|
|
43512
|
-
|
|
43513
|
-
|
|
43514
|
-
|
|
43515
|
-
|
|
43516
|
-
|
|
43517
|
-
|
|
43518
|
-
|
|
43519
|
-
|
|
43520
|
-
|
|
43521
|
-
|
|
43630
|
+
identify(pid) {
|
|
43631
|
+
const known = this.oneShots.get(pid);
|
|
43632
|
+
if (known !== undefined) {
|
|
43633
|
+
this.turnAway(pid, known);
|
|
43634
|
+
return;
|
|
43635
|
+
}
|
|
43636
|
+
const info = this.connectionInfo;
|
|
43637
|
+
const socket = this.socket;
|
|
43638
|
+
const reader = this.oneShotReader;
|
|
43639
|
+
if (info === null || reader === null) {
|
|
43640
|
+
return;
|
|
43641
|
+
}
|
|
43642
|
+
info.identifying = true;
|
|
43643
|
+
reader(pid).catch(() => {
|
|
43644
|
+
return;
|
|
43645
|
+
}).then((option) => {
|
|
43646
|
+
if (this.socket !== socket || this.connectionInfo !== info) {
|
|
43647
|
+
return;
|
|
43648
|
+
}
|
|
43649
|
+
if (typeof option === "string") {
|
|
43650
|
+
this.oneShots.set(pid, option);
|
|
43651
|
+
this.turnAway(pid, option);
|
|
43652
|
+
return;
|
|
43653
|
+
}
|
|
43654
|
+
info.identifying = false;
|
|
43655
|
+
this.log("info", `Godot editor identified: pid ${pid}`);
|
|
43656
|
+
this.emitBridgeEvent("godot_connected", { projectPath: info.projectPath });
|
|
43657
|
+
});
|
|
43522
43658
|
}
|
|
43523
|
-
|
|
43524
|
-
|
|
43525
|
-
|
|
43659
|
+
turnAway(pid, option) {
|
|
43660
|
+
const said = `pid ${pid} was started with ${option}, which does one job and quits, so it is not an editor`;
|
|
43661
|
+
this.log("info", `Turning away an engine run: ${said}`);
|
|
43662
|
+
this.turnedAway = { pid, option, at: new Date };
|
|
43663
|
+
const run = this.socket;
|
|
43664
|
+
this.socket = null;
|
|
43665
|
+
this.connectionInfo = null;
|
|
43666
|
+
this.stopKeepalive();
|
|
43667
|
+
run?.close(ONE_SHOT_CLOSE_CODE, closeReason(said, "start"));
|
|
43526
43668
|
}
|
|
43527
|
-
|
|
43528
|
-
|
|
43529
|
-
|
|
43530
|
-
|
|
43531
|
-
|
|
43669
|
+
handleDisconnect(disconnectedSocket, reason) {
|
|
43670
|
+
if (disconnectedSocket && this.socket && disconnectedSocket !== this.socket) {
|
|
43671
|
+
this.log("debug", "Ignoring stale Godot socket disconnect event");
|
|
43672
|
+
return;
|
|
43673
|
+
}
|
|
43674
|
+
this.stopKeepalive();
|
|
43675
|
+
this.socket = null;
|
|
43676
|
+
this.connectionInfo = null;
|
|
43677
|
+
this.emitBridgeEvent("godot_disconnected", {});
|
|
43678
|
+
this.rejectAllPending(reason);
|
|
43679
|
+
this.resourceQueues.clear();
|
|
43532
43680
|
}
|
|
43533
|
-
|
|
43534
|
-
|
|
43681
|
+
emitBridgeEvent(eventName, payload) {
|
|
43682
|
+
this.emit(eventName, payload);
|
|
43535
43683
|
}
|
|
43536
|
-
|
|
43537
|
-
|
|
43538
|
-
|
|
43539
|
-
|
|
43540
|
-
|
|
43541
|
-
|
|
43542
|
-
}
|
|
43543
|
-
function finishesASuite(line) {
|
|
43544
|
-
return SUITE_COUNTS.test(line) && !line.includes(OVERALL);
|
|
43545
|
-
}
|
|
43546
|
-
function suitesPrinted(printed) {
|
|
43547
|
-
const finished = [];
|
|
43548
|
-
let running = null;
|
|
43549
|
-
let failed = [];
|
|
43550
|
-
for (const line of printed) {
|
|
43551
|
-
const started = RUNNING_SUITE.exec(line);
|
|
43552
|
-
if (started?.[1] !== undefined) {
|
|
43553
|
-
running = started[1];
|
|
43554
|
-
failed = [];
|
|
43555
|
-
continue;
|
|
43684
|
+
rejectAllPending(error) {
|
|
43685
|
+
for (const pending of this.pendingRequests.values()) {
|
|
43686
|
+
clearTimeout(pending.timeout);
|
|
43687
|
+
pending.reject(error);
|
|
43688
|
+
}
|
|
43689
|
+
this.pendingRequests.clear();
|
|
43556
43690
|
}
|
|
43557
|
-
|
|
43558
|
-
|
|
43691
|
+
enqueueResourceRequest(resourceKey, task) {
|
|
43692
|
+
const previous = this.resourceQueues.get(resourceKey) ?? Promise.resolve();
|
|
43693
|
+
const taskPromise = previous.catch(() => {
|
|
43694
|
+
return;
|
|
43695
|
+
}).then(task);
|
|
43696
|
+
const tail = taskPromise.then(() => {
|
|
43697
|
+
return;
|
|
43698
|
+
}, () => {
|
|
43699
|
+
return;
|
|
43700
|
+
});
|
|
43701
|
+
this.resourceQueues.set(resourceKey, tail);
|
|
43702
|
+
return taskPromise.finally(() => {
|
|
43703
|
+
if (this.resourceQueues.get(resourceKey) === tail) {
|
|
43704
|
+
this.resourceQueues.delete(resourceKey);
|
|
43705
|
+
}
|
|
43706
|
+
});
|
|
43559
43707
|
}
|
|
43560
|
-
|
|
43561
|
-
|
|
43562
|
-
|
|
43563
|
-
|
|
43708
|
+
getResourceKey(args) {
|
|
43709
|
+
const scenePath = this.getStringArg(args, "scenePath") ?? this.getStringArg(args, "scene_path");
|
|
43710
|
+
if (scenePath) {
|
|
43711
|
+
return `scene:${scenePath}`;
|
|
43712
|
+
}
|
|
43713
|
+
const resourcePath = this.getStringArg(args, "resourcePath") ?? this.getStringArg(args, "resource_path");
|
|
43714
|
+
if (resourcePath) {
|
|
43715
|
+
return `resource:${resourcePath}`;
|
|
43716
|
+
}
|
|
43717
|
+
return;
|
|
43564
43718
|
}
|
|
43565
|
-
|
|
43566
|
-
|
|
43567
|
-
|
|
43568
|
-
path: running,
|
|
43569
|
-
cases: Number(counted[1]),
|
|
43570
|
-
errors: Number(counted[2]),
|
|
43571
|
-
failures: Number(counted[3]),
|
|
43572
|
-
failed
|
|
43573
|
-
});
|
|
43574
|
-
running = null;
|
|
43719
|
+
getStringArg(args, key) {
|
|
43720
|
+
const value = args[key];
|
|
43721
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
43575
43722
|
}
|
|
43576
|
-
|
|
43577
|
-
|
|
43578
|
-
|
|
43579
|
-
|
|
43580
|
-
|
|
43581
|
-
|
|
43582
|
-
|
|
43583
|
-
|
|
43584
|
-
|
|
43585
|
-
|
|
43586
|
-
|
|
43587
|
-
|
|
43588
|
-
|
|
43589
|
-
|
|
43590
|
-
|
|
43591
|
-
|
|
43592
|
-
|
|
43723
|
+
isIncomingMessage(value) {
|
|
43724
|
+
if (!value || typeof value !== "object") {
|
|
43725
|
+
return false;
|
|
43726
|
+
}
|
|
43727
|
+
const message = value;
|
|
43728
|
+
const type = message["type"];
|
|
43729
|
+
if (type !== "tool_result" && type !== "pong" && type !== "godot_ready") {
|
|
43730
|
+
return false;
|
|
43731
|
+
}
|
|
43732
|
+
if (type === "pong") {
|
|
43733
|
+
return true;
|
|
43734
|
+
}
|
|
43735
|
+
if (type === "godot_ready") {
|
|
43736
|
+
return typeof message["project_path"] === "string";
|
|
43737
|
+
}
|
|
43738
|
+
return typeof message["id"] === "string" && typeof message["success"] === "boolean" && (message["error"] === undefined || typeof message["error"] === "string") && (message["busy"] === undefined || typeof message["busy"] === "boolean");
|
|
43739
|
+
}
|
|
43740
|
+
log(level, message) {
|
|
43741
|
+
console.error(`[${new Date().toISOString()}] [GodotBridge:${level.toUpperCase()}] ${message}`);
|
|
43593
43742
|
}
|
|
43594
43743
|
};
|
|
43595
|
-
ENTITIES = { lt: "<", gt: ">", amp: "&", quot: '"', apos: "'" };
|
|
43596
|
-
ATTRIBUTE = /\s*([^\s=/>]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/y;
|
|
43597
|
-
HOOK_FAILURE = /^\s*(.*?)\s*\tat '(before|after)' in (res:\/\/.+?):(\d+)\s*$/;
|
|
43598
|
-
NOTHING_RAN = /no test cases found/i;
|
|
43599
|
-
NO_SUCH_PATH = /given directory or file does not exists:\s*(.+?)\s*$/i;
|
|
43600
|
-
ESCAPE = String.fromCharCode(27);
|
|
43601
|
-
PRINTED_ACTUAL = new RegExp(` but was\\n '((?:(?!\\tat ${LOCATION})[\\s\\S])*?)'((?:${ESCAPE}\\[[0-9;]*m)*)(${IGNORING_CASE})?((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (${LOCATION})`, "g");
|
|
43602
|
-
QUOTED_EQUALITY = new RegExp(`^Expecting(?: error message)?:\\n '([\\s\\S]*)'\\n but was\\n '([\\s\\S]*)'(?:${IGNORING_CASE})?\\n\\tat `);
|
|
43603
|
-
COLOUR2 = new RegExp(`${ESCAPE}\\[([0-9;]*)m`, "g");
|
|
43604
|
-
SPELLED = {
|
|
43605
|
-
"<LF>": `
|
|
43606
|
-
`,
|
|
43607
|
-
"<CR>": "\r",
|
|
43608
|
-
"<TAB>": "\t",
|
|
43609
|
-
"<BS>": "\b",
|
|
43610
|
-
"<FF>": "\f",
|
|
43611
|
-
"<VT>": "\v",
|
|
43612
|
-
"<BEL>": "\x07",
|
|
43613
|
-
"<ESC>": ESCAPE,
|
|
43614
|
-
"<DEL>": ""
|
|
43615
|
-
};
|
|
43616
|
-
PRINTED_DICTIONARY = /^\{\n\t[\s\S]*\n {2}\}$/;
|
|
43617
|
-
RUNNING_SUITE = /Run Test Suite:\s*(res:\/\/.+?\.(?:gd|cs))(?=\s|$)/;
|
|
43618
|
-
ORPHANS = /(\d+)\s+orphans/;
|
|
43619
|
-
SUITE_COUNTS = /(\d+)\s+test cases\s*\|\s*(\d+)\s+errors\s*\|\s*(\d+)\s+failures/;
|
|
43620
|
-
CASE_FAILED = /^\s*res:\/\/.+?\.(?:gd|cs) > (.+?) (?:FAILED|ERROR)(?:\s|$)/;
|
|
43621
43744
|
});
|
|
43622
43745
|
|
|
43623
43746
|
// src/keeper-channel.ts
|
|
@@ -43687,10 +43810,10 @@ var init_keeper_channel = () => {};
|
|
|
43687
43810
|
|
|
43688
43811
|
// src/lifecycle-log.ts
|
|
43689
43812
|
import { appendFileSync, mkdirSync as mkdirSync14, renameSync as renameSync5, statSync as statSync12 } from "node:fs";
|
|
43690
|
-
import { dirname as dirname20, join as
|
|
43813
|
+
import { dirname as dirname20, join as join29 } from "node:path";
|
|
43691
43814
|
import process11 from "node:process";
|
|
43692
43815
|
function lifecycleLogPath() {
|
|
43693
|
-
return
|
|
43816
|
+
return join29(runtimeDirectory(), "servers.log");
|
|
43694
43817
|
}
|
|
43695
43818
|
function writeLifecycle(event, fields = {}) {
|
|
43696
43819
|
const path = lifecycleLogPath();
|
|
@@ -44011,9 +44134,9 @@ var init_prompts = __esm(() => {
|
|
|
44011
44134
|
|
|
44012
44135
|
// src/restart-note.ts
|
|
44013
44136
|
import { mkdirSync as mkdirSync15, readFileSync as readFileSync24, rmSync as rmSync13, writeFileSync as writeFileSync17 } from "node:fs";
|
|
44014
|
-
import { dirname as dirname21, join as
|
|
44137
|
+
import { dirname as dirname21, join as join30 } from "node:path";
|
|
44015
44138
|
function restartNotePath(projectPath) {
|
|
44016
|
-
return
|
|
44139
|
+
return join30(projectPath, ".godot", "gdharness-restart.json");
|
|
44017
44140
|
}
|
|
44018
44141
|
function noteRestartBegun(note) {
|
|
44019
44142
|
const path = restartNotePath(note.projectPath);
|
|
@@ -44054,16 +44177,16 @@ var init_restart_note = __esm(() => {
|
|
|
44054
44177
|
// src/run-record.ts
|
|
44055
44178
|
import { createHash as createHash5 } from "node:crypto";
|
|
44056
44179
|
import { mkdirSync as mkdirSync16, openSync as openSync3, readdirSync as readdirSync13, readFileSync as readFileSync25, realpathSync as realpathSync6, rmSync as rmSync14, statSync as statSync13 } from "node:fs";
|
|
44057
|
-
import { basename as basename9, dirname as dirname22, join as
|
|
44180
|
+
import { basename as basename9, dirname as dirname22, join as join31, resolve as resolve7 } from "node:path";
|
|
44058
44181
|
function runsDirectory() {
|
|
44059
|
-
return
|
|
44182
|
+
return join31(runtimeDirectory(), "runs");
|
|
44060
44183
|
}
|
|
44061
44184
|
function realAsFarAsItExists(path) {
|
|
44062
44185
|
const missing = [];
|
|
44063
44186
|
let at = path;
|
|
44064
44187
|
for (;; ) {
|
|
44065
44188
|
try {
|
|
44066
|
-
return
|
|
44189
|
+
return join31(realpathSync6.native(at), ...missing);
|
|
44067
44190
|
} catch {
|
|
44068
44191
|
const parent = dirname22(at);
|
|
44069
44192
|
if (parent === at) {
|
|
@@ -44080,10 +44203,10 @@ function projectKey(projectPath) {
|
|
|
44080
44203
|
return createHash5("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
|
|
44081
44204
|
}
|
|
44082
44205
|
function runRecordPath(projectPath, directory = runtimeDirectory()) {
|
|
44083
|
-
return
|
|
44206
|
+
return join31(directory, "runs", `note-${projectKey(projectPath)}.json`);
|
|
44084
44207
|
}
|
|
44085
44208
|
function latestRunPath(servedBy, directory = runtimeDirectory()) {
|
|
44086
|
-
return
|
|
44209
|
+
return join31(directory, "runs", `latest-${projectKey(servedBy)}.json`);
|
|
44087
44210
|
}
|
|
44088
44211
|
function writeLatestRun(servedBy, latest) {
|
|
44089
44212
|
try {
|
|
@@ -44108,20 +44231,20 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
|
|
|
44108
44231
|
}
|
|
44109
44232
|
function openTranscript(startedAt) {
|
|
44110
44233
|
mkdirSync16(runsDirectory(), { recursive: true });
|
|
44111
|
-
const path =
|
|
44234
|
+
const path = join31(runsDirectory(), `run-${startedAt}.log`);
|
|
44112
44235
|
return { path, fd: openSync3(path, "a") };
|
|
44113
44236
|
}
|
|
44114
44237
|
function profilePath(startedAt) {
|
|
44115
|
-
return
|
|
44238
|
+
return join31(runsDirectory(), `run-${startedAt}.profile.json`);
|
|
44116
44239
|
}
|
|
44117
44240
|
function profileMarkPath(startedAt) {
|
|
44118
|
-
return
|
|
44241
|
+
return join31(runsDirectory(), `run-${startedAt}.profile-mark.json`);
|
|
44119
44242
|
}
|
|
44120
44243
|
function noteIsAbout(note, playing) {
|
|
44121
44244
|
return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
|
|
44122
44245
|
}
|
|
44123
44246
|
function editorNotePath(projectPath, directory = runtimeDirectory()) {
|
|
44124
|
-
return
|
|
44247
|
+
return join31(directory, "runs", `editor-${projectKey(projectPath)}.json`);
|
|
44125
44248
|
}
|
|
44126
44249
|
function writeEditorRunNote(note) {
|
|
44127
44250
|
mkdirSync16(runsDirectory(), { recursive: true });
|
|
@@ -44132,7 +44255,7 @@ function writeEditorRunNote(note) {
|
|
|
44132
44255
|
}
|
|
44133
44256
|
function readEditorRunNote(projectPath) {
|
|
44134
44257
|
for (const directory of runtimeDirectories()) {
|
|
44135
|
-
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(
|
|
44258
|
+
const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join31(directory, "runs", "editor-run.json"));
|
|
44136
44259
|
if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
|
|
44137
44260
|
return note;
|
|
44138
44261
|
}
|
|
@@ -44166,7 +44289,7 @@ function readRunRecord(projectPath) {
|
|
|
44166
44289
|
if (own !== null) {
|
|
44167
44290
|
return own;
|
|
44168
44291
|
}
|
|
44169
|
-
const shared = recordAt(
|
|
44292
|
+
const shared = recordAt(join31(directory, "runs", SHARED_NOTE));
|
|
44170
44293
|
if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
|
|
44171
44294
|
return shared;
|
|
44172
44295
|
}
|
|
@@ -44178,13 +44301,13 @@ function everyRunRecord() {
|
|
|
44178
44301
|
for (const directory of runtimeDirectories()) {
|
|
44179
44302
|
let entries;
|
|
44180
44303
|
try {
|
|
44181
|
-
entries = readdirSync13(
|
|
44304
|
+
entries = readdirSync13(join31(directory, "runs"));
|
|
44182
44305
|
} catch {
|
|
44183
44306
|
continue;
|
|
44184
44307
|
}
|
|
44185
44308
|
for (const entry of entries) {
|
|
44186
44309
|
if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
|
|
44187
|
-
const record = recordAt(
|
|
44310
|
+
const record = recordAt(join31(directory, "runs", entry));
|
|
44188
44311
|
if (record !== null) {
|
|
44189
44312
|
found.push(record);
|
|
44190
44313
|
}
|
|
@@ -44392,7 +44515,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
|
|
|
44392
44515
|
}
|
|
44393
44516
|
function clearRunRecord(projectPath) {
|
|
44394
44517
|
for (const directory of runtimeDirectories()) {
|
|
44395
|
-
const shared =
|
|
44518
|
+
const shared = join31(directory, "runs", SHARED_NOTE);
|
|
44396
44519
|
const sharedRecord = recordAt(shared);
|
|
44397
44520
|
const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
|
|
44398
44521
|
for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
|
|
@@ -44427,7 +44550,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
|
|
|
44427
44550
|
if (!transcript && !entry.endsWith(".partial")) {
|
|
44428
44551
|
continue;
|
|
44429
44552
|
}
|
|
44430
|
-
const path =
|
|
44553
|
+
const path = join31(directory, entry);
|
|
44431
44554
|
if (kept.has(path) || ageMs(path, now) < keepMs) {
|
|
44432
44555
|
continue;
|
|
44433
44556
|
}
|
|
@@ -44447,110 +44570,6 @@ var init_run_record = __esm(() => {
|
|
|
44447
44570
|
init_windows_ask();
|
|
44448
44571
|
});
|
|
44449
44572
|
|
|
44450
|
-
// src/script-origins.ts
|
|
44451
|
-
import { join as join31 } from "node:path";
|
|
44452
|
-
function unresolvedClassIn(message) {
|
|
44453
|
-
return UNRESOLVED.exec(message)?.[1] ?? null;
|
|
44454
|
-
}
|
|
44455
|
-
async function originsOf(errors, declared, check) {
|
|
44456
|
-
const listed = new Set(errors.map((error) => error.path));
|
|
44457
|
-
const origins = [];
|
|
44458
|
-
const unconfirmed = [];
|
|
44459
|
-
const asked = new Set;
|
|
44460
|
-
const waiting = errors.map((error) => unresolvedClassIn(error.message)).filter((name) => name !== null);
|
|
44461
|
-
while (waiting.length > 0 && asked.size < MOST_CHECKED) {
|
|
44462
|
-
const name = waiting.shift() ?? "";
|
|
44463
|
-
const path = declared.get(name);
|
|
44464
|
-
if (path === undefined || listed.has(path) || asked.has(path)) {
|
|
44465
|
-
continue;
|
|
44466
|
-
}
|
|
44467
|
-
asked.add(path);
|
|
44468
|
-
const checked = await check(path);
|
|
44469
|
-
const own = checked.errors.filter((error) => error.path === path);
|
|
44470
|
-
if (own.length === 0) {
|
|
44471
|
-
unconfirmed.push({
|
|
44472
|
-
path,
|
|
44473
|
-
declares: name,
|
|
44474
|
-
why: checked.failure === null ? "the engine's check of it found no error" : `the engine's check of it did not finish (${checked.failure})`
|
|
44475
|
-
});
|
|
44476
|
-
continue;
|
|
44477
|
-
}
|
|
44478
|
-
for (const error of own) {
|
|
44479
|
-
origins.push({ ...error, declares: name });
|
|
44480
|
-
const deeper = unresolvedClassIn(error.message);
|
|
44481
|
-
if (deeper !== null) {
|
|
44482
|
-
waiting.push(deeper);
|
|
44483
|
-
}
|
|
44484
|
-
}
|
|
44485
|
-
}
|
|
44486
|
-
return { origins, unconfirmed };
|
|
44487
|
-
}
|
|
44488
|
-
function compileErrorsIn(entries) {
|
|
44489
|
-
return entries.flatMap((entry) => {
|
|
44490
|
-
const at = entry.severity === "error" ? reloadedAt(entry) : null;
|
|
44491
|
-
return at === null ? [] : [{ ...at, message: entry.text }];
|
|
44492
|
-
});
|
|
44493
|
-
}
|
|
44494
|
-
function reloadedAt(entry) {
|
|
44495
|
-
for (const line of entry.detail) {
|
|
44496
|
-
const at = RELOADED_AT.exec(line.trim());
|
|
44497
|
-
if (at !== null) {
|
|
44498
|
-
return { path: at[1] ?? "", line: Number(at[2]) === 0 ? null : Number(at[2]) };
|
|
44499
|
-
}
|
|
44500
|
-
}
|
|
44501
|
-
return null;
|
|
44502
|
-
}
|
|
44503
|
-
function checkWithEngine(godotPath, projectPath) {
|
|
44504
|
-
return async (path) => {
|
|
44505
|
-
const logDir = scratchDirectory("tests");
|
|
44506
|
-
try {
|
|
44507
|
-
const ran = await runEngine(godotPath, [
|
|
44508
|
-
"--headless",
|
|
44509
|
-
"--log-file",
|
|
44510
|
-
join31(logDir, "engine.log"),
|
|
44511
|
-
"--path",
|
|
44512
|
-
projectPath,
|
|
44513
|
-
"--check-only",
|
|
44514
|
-
"-s",
|
|
44515
|
-
path
|
|
44516
|
-
], { timeout: { ms: 60000, said: "minute" } });
|
|
44517
|
-
const errors = ran.log.select({ severity: "error", sinceLastCall: false, limit: 50 }).entries.flatMap((entry) => {
|
|
44518
|
-
const at = reloadedAt(entry);
|
|
44519
|
-
return at === null ? [] : [{ ...at, message: entry.text }];
|
|
44520
|
-
});
|
|
44521
|
-
return { errors, failure: ran.failure === null ? null : howItEnded(ran) };
|
|
44522
|
-
} finally {
|
|
44523
|
-
discard(logDir);
|
|
44524
|
-
}
|
|
44525
|
-
};
|
|
44526
|
-
}
|
|
44527
|
-
function withoutListed(entries, errors) {
|
|
44528
|
-
if (errors.length === 0) {
|
|
44529
|
-
return [...entries];
|
|
44530
|
-
}
|
|
44531
|
-
const listing = discoveryListing(entries.map((entry) => entry.text));
|
|
44532
|
-
const paths = new Set(errors.map((error) => error.path));
|
|
44533
|
-
return entries.filter((entry, at) => {
|
|
44534
|
-
if (listing !== null && at >= listing.from && at < listing.to) {
|
|
44535
|
-
return false;
|
|
44536
|
-
}
|
|
44537
|
-
const reloaded = reloadedAt(entry);
|
|
44538
|
-
if (reloaded !== null && errors.some((error) => error.path === reloaded.path && error.line === reloaded.line && error.message === entry.text)) {
|
|
44539
|
-
return false;
|
|
44540
|
-
}
|
|
44541
|
-
const failedToLoad = /^Failed to load script "(res:\/\/[^"]+)" with error "[^"]+"\.$/.exec(entry.text);
|
|
44542
|
-
return failedToLoad === null || !paths.has(failedToLoad[1] ?? "");
|
|
44543
|
-
});
|
|
44544
|
-
}
|
|
44545
|
-
var UNRESOLVED, MOST_CHECKED = 8, RELOADED_AT;
|
|
44546
|
-
var init_script_origins = __esm(() => {
|
|
44547
|
-
init_engine_run();
|
|
44548
|
-
init_junit();
|
|
44549
|
-
init_scratch();
|
|
44550
|
-
UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
|
|
44551
|
-
RELOADED_AT = /GDScript::reload \((res:\/\/.+):(\d+)\)$/;
|
|
44552
|
-
});
|
|
44553
|
-
|
|
44554
44573
|
// src/script-profile.ts
|
|
44555
44574
|
import { readFileSync as readFileSync26 } from "node:fs";
|
|
44556
44575
|
function readProfileFile(path) {
|
|
@@ -45660,7 +45679,8 @@ function repeatingScriptError(entries, byScript) {
|
|
|
45660
45679
|
if (last === undefined || !byScript(last)) {
|
|
45661
45680
|
return null;
|
|
45662
45681
|
}
|
|
45663
|
-
const
|
|
45682
|
+
const whole = last.detail.length > 0 ? last : entries.findLast((entry) => entry.text === last.text && entry.detail.length > 0 && byScript(entry)) ?? last;
|
|
45683
|
+
const where = whole.detail[0] ?? null;
|
|
45664
45684
|
const times = entries.filter((entry) => entry.text === last.text && (entry.detail[0] ?? null) === where && byScript(entry)).length;
|
|
45665
45685
|
return times >= LOOPING_AFTER ? { text: last.text, where: where?.replace(/^at: /, "") ?? null, times } : null;
|
|
45666
45686
|
}
|
|
@@ -46056,9 +46076,9 @@ function scriptsUnder(directory, prefix) {
|
|
|
46056
46076
|
}
|
|
46057
46077
|
return found;
|
|
46058
46078
|
}
|
|
46059
|
-
function
|
|
46079
|
+
function mainSceneOf(projectFile) {
|
|
46060
46080
|
const scene = parseProjectGodot(readFileSync27(projectFile, "utf8"))["application"]?.["run/main_scene"];
|
|
46061
|
-
return typeof scene === "string" && scene !== "";
|
|
46081
|
+
return typeof scene === "string" && scene !== "" ? scene : null;
|
|
46062
46082
|
}
|
|
46063
46083
|
function runtimeAddonAnnouncesEditors(projectPath) {
|
|
46064
46084
|
if (projectPath === null) {
|
|
@@ -46888,13 +46908,13 @@ class GodotServer {
|
|
|
46888
46908
|
stateMachinePath: readString(args, "stateMachinePath") ?? ""
|
|
46889
46909
|
}), async () => await bridge("read_animation_states"));
|
|
46890
46910
|
case "create":
|
|
46891
|
-
return await
|
|
46911
|
+
return await editorOrEngine("create_animation", "writes");
|
|
46892
46912
|
case "add_track":
|
|
46893
|
-
return await
|
|
46913
|
+
return await editorOrEngine("add_animation_track", "writes");
|
|
46894
46914
|
case "add_state":
|
|
46895
|
-
return await
|
|
46915
|
+
return await editorOrEngine("add_animation_state", "writes");
|
|
46896
46916
|
default:
|
|
46897
|
-
return await
|
|
46917
|
+
return await editorOrEngine("connect_animation_states", "writes");
|
|
46898
46918
|
}
|
|
46899
46919
|
case "script_edit":
|
|
46900
46920
|
return await this.handleRenameSymbol(args);
|
|
@@ -48038,6 +48058,7 @@ class GodotServer {
|
|
|
48038
48058
|
if (!engine.ok) {
|
|
48039
48059
|
return engine.response;
|
|
48040
48060
|
}
|
|
48061
|
+
const began = Date.now();
|
|
48041
48062
|
const classes = await this.rebuildClassCache(project.value.path);
|
|
48042
48063
|
if (!classes.ok) {
|
|
48043
48064
|
return this.answer(classes);
|
|
@@ -48111,6 +48132,7 @@ class GodotServer {
|
|
|
48111
48132
|
await untilGone([run.process.pid]);
|
|
48112
48133
|
}
|
|
48113
48134
|
await run.settled;
|
|
48135
|
+
const tookMs = Date.now() - began;
|
|
48114
48136
|
const notCompiled = stopped.for ?? run.log.all.map((entry) => autoloadNotCompiled(entry, autoloaded)).find((found) => found !== null) ?? null;
|
|
48115
48137
|
const reportsDir = join32(project.value.path, ".godot", ...ours.split("/"));
|
|
48116
48138
|
let report = null;
|
|
@@ -48200,6 +48222,7 @@ class GodotServer {
|
|
|
48200
48222
|
passed: false,
|
|
48201
48223
|
verdict,
|
|
48202
48224
|
tests: 0,
|
|
48225
|
+
tookMs,
|
|
48203
48226
|
exitCode,
|
|
48204
48227
|
exitSignal: run.exitSignal ?? undefined,
|
|
48205
48228
|
hung,
|
|
@@ -48257,6 +48280,7 @@ class GodotServer {
|
|
|
48257
48280
|
errors: report.errors,
|
|
48258
48281
|
skipped: report.skipped,
|
|
48259
48282
|
time: report.time,
|
|
48283
|
+
tookMs,
|
|
48260
48284
|
failed,
|
|
48261
48285
|
failedWithoutDetail: briefed > 0 ? briefed : undefined,
|
|
48262
48286
|
hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
|
|
@@ -49363,7 +49387,7 @@ class GodotServer {
|
|
|
49363
49387
|
if (!asked.ok) {
|
|
49364
49388
|
return asked.response;
|
|
49365
49389
|
}
|
|
49366
|
-
if (exported === null && !sceneToRun &&
|
|
49390
|
+
if (exported === null && !sceneToRun && mainSceneOf(project.value.file) === null) {
|
|
49367
49391
|
return this.createErrorResponse("The project sets no main scene, so there is nothing to run.", [
|
|
49368
49392
|
"Pass scene to run one scene",
|
|
49369
49393
|
"Choose the main scene with project_settings set_main_scene"
|
|
@@ -49440,6 +49464,7 @@ class GodotServer {
|
|
|
49440
49464
|
alsoRunning: stranded.length === 0 ? undefined : stranded.map((one) => one.pid),
|
|
49441
49465
|
pid: started.pid,
|
|
49442
49466
|
executable: exported?.ok === true ? exported.relativePath : undefined,
|
|
49467
|
+
scene: exported !== null ? undefined : sceneArgument === null ? mainSceneOf(project.value.file) : `res://${sceneArgument}`,
|
|
49443
49468
|
arguments: cmdArgs,
|
|
49444
49469
|
argsNote: engineFlagsNote(given.value),
|
|
49445
49470
|
transcript: started.transcript,
|
|
@@ -53360,9 +53385,9 @@ init_setup();
|
|
|
53360
53385
|
init_tool_definitions();
|
|
53361
53386
|
init_tool_reference();
|
|
53362
53387
|
import { existsSync as existsSync14, mkdirSync as mkdirSync5, readdirSync as readdirSync9, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
|
|
53363
|
-
import { basename as basename6, dirname as dirname8, join as
|
|
53388
|
+
import { basename as basename6, dirname as dirname8, join as join15 } from "node:path";
|
|
53364
53389
|
var SKILL_NAME = "gdharness";
|
|
53365
|
-
var SHARED_SKILLS =
|
|
53390
|
+
var SHARED_SKILLS = join15(".agents", "skills");
|
|
53366
53391
|
function skillMarkdown(version) {
|
|
53367
53392
|
return `---
|
|
53368
53393
|
name: ${SKILL_NAME}
|
|
@@ -53587,27 +53612,27 @@ what they report while starting comes back under \`engine_messages_before_operat
|
|
|
53587
53612
|
function skillFiles(version) {
|
|
53588
53613
|
return new Map([
|
|
53589
53614
|
["SKILL.md", skillMarkdown(version)],
|
|
53590
|
-
[
|
|
53615
|
+
[join15("references", "tools.md"), renderToolsMarkdown()]
|
|
53591
53616
|
]);
|
|
53592
53617
|
}
|
|
53593
53618
|
function skillDirectories(harnesses, projectPath, exists) {
|
|
53594
|
-
const directories = [
|
|
53619
|
+
const directories = [join15(projectPath, SHARED_SKILLS, SKILL_NAME)];
|
|
53595
53620
|
for (const harness of harnesses) {
|
|
53596
53621
|
if (harness.skills === undefined) {
|
|
53597
53622
|
continue;
|
|
53598
53623
|
}
|
|
53599
|
-
const own =
|
|
53600
|
-
if ((!harness.skills.shared || exists(own)) && !directories.includes(
|
|
53601
|
-
directories.push(
|
|
53624
|
+
const own = join15(projectPath, harness.skills.dir);
|
|
53625
|
+
if ((!harness.skills.shared || exists(own)) && !directories.includes(join15(own, SKILL_NAME))) {
|
|
53626
|
+
directories.push(join15(own, SKILL_NAME));
|
|
53602
53627
|
}
|
|
53603
53628
|
}
|
|
53604
53629
|
return directories;
|
|
53605
53630
|
}
|
|
53606
53631
|
function everySkillDirectory(projectPath, harnesses) {
|
|
53607
|
-
const directories = new Set([
|
|
53632
|
+
const directories = new Set([join15(projectPath, SHARED_SKILLS, SKILL_NAME)]);
|
|
53608
53633
|
for (const harness of harnesses) {
|
|
53609
53634
|
if (harness.skills !== undefined) {
|
|
53610
|
-
directories.add(
|
|
53635
|
+
directories.add(join15(projectPath, harness.skills.dir, SKILL_NAME));
|
|
53611
53636
|
}
|
|
53612
53637
|
}
|
|
53613
53638
|
return [...directories];
|
|
@@ -53624,7 +53649,7 @@ function removeSkill(directories, projectPath) {
|
|
|
53624
53649
|
return removed;
|
|
53625
53650
|
}
|
|
53626
53651
|
function pruneEmpty(directory, projectPath) {
|
|
53627
|
-
const shared =
|
|
53652
|
+
const shared = join15(projectPath, ".agents");
|
|
53628
53653
|
let path = directory;
|
|
53629
53654
|
while (path !== projectPath && (basename6(path) === "skills" || path === shared)) {
|
|
53630
53655
|
if (readdirSync9(path).length > 0) {
|
|
@@ -53640,7 +53665,7 @@ function writeSkill(directories, version) {
|
|
|
53640
53665
|
const replaced = existsSync14(directory);
|
|
53641
53666
|
rmSync6(directory, { recursive: true, force: true });
|
|
53642
53667
|
for (const [name, contents] of files) {
|
|
53643
|
-
const path =
|
|
53668
|
+
const path = join15(directory, name);
|
|
53644
53669
|
mkdirSync5(dirname8(path), { recursive: true });
|
|
53645
53670
|
writeFileSync7(path, contents, "utf8");
|
|
53646
53671
|
}
|
|
@@ -53650,13 +53675,13 @@ function writeSkill(directories, version) {
|
|
|
53650
53675
|
|
|
53651
53676
|
// src/version.ts
|
|
53652
53677
|
import { readFileSync as readFileSync12 } from "node:fs";
|
|
53653
|
-
import { dirname as dirname9, join as
|
|
53678
|
+
import { dirname as dirname9, join as join16 } from "node:path";
|
|
53654
53679
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
53655
53680
|
function getLocalVersion() {
|
|
53656
53681
|
const currentDirectory = dirname9(fileURLToPath2(import.meta.url));
|
|
53657
53682
|
const packageCandidates = [
|
|
53658
|
-
|
|
53659
|
-
|
|
53683
|
+
join16(currentDirectory, "..", "package.json"),
|
|
53684
|
+
join16(currentDirectory, "..", "..", "package.json")
|
|
53660
53685
|
];
|
|
53661
53686
|
for (const packagePath of packageCandidates) {
|
|
53662
53687
|
try {
|