gdharness 1.2.36 → 1.2.38

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/index.js CHANGED
@@ -3365,17 +3365,18 @@ import {
3365
3365
  closeSync as closeSync3,
3366
3366
  constants,
3367
3367
  copyFileSync,
3368
- existsSync as existsSync20,
3368
+ existsSync as existsSync21,
3369
3369
  fstatSync,
3370
3370
  mkdirSync as mkdirSync15,
3371
3371
  openSync as openSync4,
3372
- readdirSync as readdirSync11,
3372
+ readdirSync as readdirSync12,
3373
3373
  readFileSync as readFileSync24,
3374
3374
  readSync as readSync2,
3375
3375
  rmdirSync as rmdirSync2,
3376
- statSync as statSync12
3376
+ statSync as statSync13,
3377
+ writeFileSync as writeFileSync16
3377
3378
  } from "node:fs";
3378
- import { basename as basename7, dirname as dirname18, extname as extname4, isAbsolute as isAbsolute4, join as join26, normalize as normalize2, relative as relative5 } from "node:path";
3379
+ import { basename as basename8, dirname as dirname19, extname as extname4, isAbsolute as isAbsolute5, join as join27, normalize as normalize2, relative as relative5 } from "node:path";
3379
3380
  import { setTimeout as delay5 } from "node:timers/promises";
3380
3381
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3381
3382
 
@@ -28537,19 +28538,11 @@ function dictionary(entries) {
28537
28538
  return Object.assign(emptyRecord(), entries);
28538
28539
  }
28539
28540
 
28540
- // src/editor-log.ts
28541
- import {
28542
- closeSync as closeSync2,
28543
- existsSync as existsSync7,
28544
- mkdirSync as mkdirSync7,
28545
- openSync as openSync2,
28546
- readFileSync as readFileSync12,
28547
- readSync,
28548
- rmSync as rmSync6,
28549
- statSync as statSync5,
28550
- writeFileSync as writeFileSync8
28551
- } from "node:fs";
28552
- import { dirname as dirname7, join as join11 } from "node:path";
28541
+ // src/doc-cache.ts
28542
+ import { existsSync as existsSync7, readdirSync as readdirSync3, statSync as statSync5 } from "node:fs";
28543
+ import { homedir as homedir2 } from "node:os";
28544
+ import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute3, join as join11 } from "node:path";
28545
+ import process7 from "node:process";
28553
28546
 
28554
28547
  // src/game-log.ts
28555
28548
  import { StringDecoder } from "node:string_decoder";
@@ -28757,6 +28750,9 @@ class GameLog {
28757
28750
  count(severity) {
28758
28751
  return this.entries.filter((entry) => entry.severity === severity).length;
28759
28752
  }
28753
+ crashReport() {
28754
+ return this.entries.find((entry) => CRASH.test(entry.text));
28755
+ }
28760
28756
  everything() {
28761
28757
  return this.entries;
28762
28758
  }
@@ -28790,12 +28786,131 @@ function answersTo(entry, severity, contains) {
28790
28786
  return [entry.text, ...entry.detail].some((line) => line.toLowerCase().includes(needle));
28791
28787
  }
28792
28788
 
28789
+ // src/engine-run.ts
28790
+ function runEngine(executable, args, options = {}) {
28791
+ return new Promise((resolve) => {
28792
+ const log = new GameLog;
28793
+ let failure = null;
28794
+ const signal = callSignal();
28795
+ const child = spawnHidden(executable, args, {
28796
+ stdio: ["ignore", "pipe", "pipe"],
28797
+ ...signal === undefined ? {} : { signal }
28798
+ });
28799
+ child.stdout.on("data", (chunk) => {
28800
+ log.append("stdout", chunk);
28801
+ });
28802
+ child.stderr.on("data", (chunk) => {
28803
+ log.append("stderr", chunk);
28804
+ });
28805
+ const { timeout } = options;
28806
+ const timer = timeout === undefined ? undefined : setTimeout(() => {
28807
+ failure ??= `it ran past its ${timeout.said}`;
28808
+ child.kill();
28809
+ }, timeout.ms);
28810
+ child.on("error", (error) => {
28811
+ failure ??= error.name === "AbortError" ? "the call was cancelled" : `it could not be started: ${error.message}`;
28812
+ });
28813
+ child.on("close", (code, closedBy) => {
28814
+ clearTimeout(timer);
28815
+ log.finish();
28816
+ const started = child.pid !== undefined;
28817
+ resolve({ log, exitCode: started ? code : null, exitSignal: closedBy, failure });
28818
+ });
28819
+ });
28820
+ }
28821
+ var WINDOWS_STATUSES = new Map([
28822
+ [3221225477, "an access violation, so it crashed"],
28823
+ [3221225501, "an illegal instruction, so it crashed"],
28824
+ [3221225620, "an integer division by zero, so it crashed"],
28825
+ [3221225725, "a stack overflow, so it crashed"],
28826
+ [3221225781, "a library it needs was not found, so it never started"],
28827
+ [3221225786, "its console was closed or sent Ctrl+C"],
28828
+ [3221226356, "a corrupted heap, so it crashed"],
28829
+ [3221226505, "a stack buffer overrun or a fast fail, so it crashed"]
28830
+ ]);
28831
+ function exitCodeText(code) {
28832
+ if (code === null) {
28833
+ return "exit code unknown";
28834
+ }
28835
+ if (code < 3221225472) {
28836
+ return `exit code ${code}`;
28837
+ }
28838
+ const hex = `0x${code.toString(16).toUpperCase()}`;
28839
+ return `exit code ${hex}, ${WINDOWS_STATUSES.get(code) ?? "a Windows error status"}`;
28840
+ }
28841
+ function howItEnded(run) {
28842
+ if (run.failure !== null) {
28843
+ return run.exitSignal === null ? run.failure : `ended by ${run.exitSignal}, because ${run.failure}`;
28844
+ }
28845
+ return run.exitSignal === null ? exitCodeText(run.exitCode) : `ended by ${run.exitSignal}`;
28846
+ }
28847
+
28848
+ // src/doc-cache.ts
28849
+ var CACHE_FILE = /^editor_doc_cache-\d+\.\d+\.res$/;
28850
+ function editorCacheDirectory(godotPath, platform = process7.platform, env = process7.env, home = homedir2()) {
28851
+ const beside = dirname7(godotPath);
28852
+ const bundle = platform === "darwin" && basename2(beside) === "MacOS" && basename2(dirname7(beside)) === "Contents" ? dirname7(dirname7(dirname7(beside))) : null;
28853
+ for (const folder of bundle === null ? [beside] : [beside, bundle]) {
28854
+ if (existsSync7(join11(folder, "._sc_")) || existsSync7(join11(folder, "_sc_"))) {
28855
+ return join11(folder, "editor_data", "cache");
28856
+ }
28857
+ }
28858
+ if (platform === "win32") {
28859
+ const set = (name) => env[name] === "" ? undefined : env[name];
28860
+ return join11(set("LOCALAPPDATA") ?? set("TEMP") ?? join11(home, "AppData", "Local"), "Godot");
28861
+ }
28862
+ if (platform === "darwin") {
28863
+ return join11(home, "Library", "Caches", "Godot");
28864
+ }
28865
+ const xdg = env["XDG_CACHE_HOME"];
28866
+ return join11(xdg !== undefined && isAbsolute3(xdg) ? xdg : join11(home, ".cache"), "godot");
28867
+ }
28868
+ function docCachesWritten(directory) {
28869
+ const written = new Map;
28870
+ let names;
28871
+ try {
28872
+ names = readdirSync3(directory);
28873
+ } catch {
28874
+ return written;
28875
+ }
28876
+ for (const name of names.filter((one) => CACHE_FILE.test(one))) {
28877
+ try {
28878
+ written.set(join11(directory, name), statSync5(join11(directory, name)).mtimeMs);
28879
+ } catch {}
28880
+ }
28881
+ return written;
28882
+ }
28883
+ function docCacheCrash(ran, before, after, what) {
28884
+ const report = ran.log.crashReport();
28885
+ if (ran.exitCode === 0 && ran.exitSignal === null && report === undefined || ran.failure !== null) {
28886
+ return null;
28887
+ }
28888
+ const rewritten = [...after].find(([path, at]) => before.get(path) !== at)?.[0];
28889
+ if (rewritten === undefined) {
28890
+ return null;
28891
+ }
28892
+ const said = report === undefined ? "" : `, and its crash handler printed "${report.text}"`;
28893
+ return `The first ${what} ended badly (${howItEnded(ran)}${said}) after rebuilding the editor's class reference ` + `cache, ${rewritten}. Godot crashes as an import or an export exits when it finds that cache ` + "stale, which it is whenever another build of the same minor version wrote it last, such as 4.7.1 " + `beside 4.7.2, and the crashing run writes a fresh one. So the ${what} was run again, and this answer ` + "is from that second run.";
28894
+ }
28895
+
28793
28896
  // src/editor-log.ts
28897
+ import {
28898
+ closeSync as closeSync2,
28899
+ existsSync as existsSync8,
28900
+ mkdirSync as mkdirSync7,
28901
+ openSync as openSync2,
28902
+ readFileSync as readFileSync12,
28903
+ readSync,
28904
+ rmSync as rmSync6,
28905
+ statSync as statSync6,
28906
+ writeFileSync as writeFileSync8
28907
+ } from "node:fs";
28908
+ import { dirname as dirname8, join as join12 } from "node:path";
28794
28909
  function editorLogPath(projectPath) {
28795
- return join11(projectPath, ".godot", "gdharness-editor.log");
28910
+ return join12(projectPath, ".godot", "gdharness-editor.log");
28796
28911
  }
28797
28912
  function editorLogNotePath(projectPath) {
28798
- return join11(projectPath, ".godot", "gdharness-editor.json");
28913
+ return join12(projectPath, ".godot", "gdharness-editor.json");
28799
28914
  }
28800
28915
  function readEditorLogNote(projectPath) {
28801
28916
  try {
@@ -28810,7 +28925,7 @@ function readEditorLogNote(projectPath) {
28810
28925
  function clearEditorLog(projectPath) {
28811
28926
  try {
28812
28927
  const path = editorLogPath(projectPath);
28813
- mkdirSync7(dirname7(path), { recursive: true });
28928
+ mkdirSync7(dirname8(path), { recursive: true });
28814
28929
  rmSync6(path, { force: true });
28815
28930
  rmSync6(editorLogNotePath(projectPath), { force: true });
28816
28931
  } catch {}
@@ -28818,7 +28933,7 @@ function clearEditorLog(projectPath) {
28818
28933
  function writeEditorLogNote(projectPath, pid) {
28819
28934
  try {
28820
28935
  const path = editorLogNotePath(projectPath);
28821
- mkdirSync7(dirname7(path), { recursive: true });
28936
+ mkdirSync7(dirname8(path), { recursive: true });
28822
28937
  writeFileSync8(path, JSON.stringify({ pid, startedAt: new Date().toISOString() }, null, 2), "utf8");
28823
28938
  } catch {}
28824
28939
  }
@@ -28840,7 +28955,7 @@ function editorConsole(projectPath, editorPid, openedByAServer) {
28840
28955
  return { kind: "older server", notedPid: noted?.pid ?? null };
28841
28956
  }
28842
28957
  const path = editorLogPath(projectPath);
28843
- if (!existsSync7(path)) {
28958
+ if (!existsSync8(path)) {
28844
28959
  return { kind: "not written" };
28845
28960
  }
28846
28961
  const log = new GameLog;
@@ -28899,7 +29014,7 @@ function foldProgress(text) {
28899
29014
  }
28900
29015
  var MOST_CONSOLE_BYTES = 8 * 1024 * 1024;
28901
29016
  function theFirstOf(path) {
28902
- const whole = statSync5(path).size;
29017
+ const whole = statSync6(path).size;
28903
29018
  if (whole <= MOST_CONSOLE_BYTES) {
28904
29019
  return { text: readFileSync12(path, "utf8") };
28905
29020
  }
@@ -29056,60 +29171,21 @@ function linesBefore(entries, at, collapsed, wanted) {
29056
29171
  return found.reverse();
29057
29172
  }
29058
29173
 
29059
- // src/engine-run.ts
29060
- function runEngine(executable, args, options = {}) {
29061
- return new Promise((resolve) => {
29062
- const log = new GameLog;
29063
- let failure = null;
29064
- const signal = callSignal();
29065
- const child = spawnHidden(executable, args, {
29066
- stdio: ["ignore", "pipe", "pipe"],
29067
- ...signal === undefined ? {} : { signal }
29068
- });
29069
- child.stdout.on("data", (chunk) => {
29070
- log.append("stdout", chunk);
29071
- });
29072
- child.stderr.on("data", (chunk) => {
29073
- log.append("stderr", chunk);
29074
- });
29075
- const { timeout } = options;
29076
- const timer = timeout === undefined ? undefined : setTimeout(() => {
29077
- failure ??= `it ran past its ${timeout.said}`;
29078
- child.kill();
29079
- }, timeout.ms);
29080
- child.on("error", (error) => {
29081
- failure ??= error.name === "AbortError" ? "the call was cancelled" : `it could not be started: ${error.message}`;
29082
- });
29083
- child.on("close", (code, closedBy) => {
29084
- clearTimeout(timer);
29085
- log.finish();
29086
- const started = child.pid !== undefined;
29087
- resolve({ log, exitCode: started ? code : null, exitSignal: closedBy, failure });
29088
- });
29089
- });
29090
- }
29091
- function howItEnded(run) {
29092
- if (run.failure !== null) {
29093
- return run.exitSignal === null ? run.failure : `ended by ${run.exitSignal}, because ${run.failure}`;
29094
- }
29095
- return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}`;
29096
- }
29097
-
29098
29174
  // src/file-move.ts
29099
29175
  import {
29100
- existsSync as existsSync12,
29176
+ existsSync as existsSync13,
29101
29177
  mkdirSync as mkdirSync10,
29102
- readdirSync as readdirSync5,
29178
+ readdirSync as readdirSync6,
29103
29179
  readFileSync as readFileSync16,
29104
29180
  renameSync as renameSync3,
29105
- statSync as statSync6,
29181
+ statSync as statSync7,
29106
29182
  writeFileSync as writeFileSync11
29107
29183
  } from "node:fs";
29108
- import { basename as basename3, dirname as dirname11, extname as extname3, join as join15, relative as relative4 } from "node:path";
29184
+ import { basename as basename4, dirname as dirname12, extname as extname3, join as join16, relative as relative4 } from "node:path";
29109
29185
 
29110
29186
  // src/project-scan.ts
29111
- import { existsSync as existsSync8, readdirSync as readdirSync3, readFileSync as readFileSync13 } from "node:fs";
29112
- import { extname, join as join12 } from "node:path";
29187
+ import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync13 } from "node:fs";
29188
+ import { extname, join as join13 } from "node:path";
29113
29189
  var SKIPPED = new Set(["node_modules"]);
29114
29190
  function skipped(name) {
29115
29191
  return name.startsWith(".") || SKIPPED.has(name);
@@ -29119,10 +29195,10 @@ function steppedOver(directory) {
29119
29195
  return gdignored(directory) || installedByUs(directory);
29120
29196
  }
29121
29197
  function gdignored(directory) {
29122
- return existsSync8(join12(directory, ".gdignore"));
29198
+ return existsSync9(join13(directory, ".gdignore"));
29123
29199
  }
29124
29200
  function installedByUs(directory) {
29125
- return existsSync8(join12(directory, VERSION_MARKER));
29201
+ return existsSync9(join13(directory, VERSION_MARKER));
29126
29202
  }
29127
29203
  var SCENE_EXTENSIONS = new Set(["tscn", "scn"]);
29128
29204
  var ASSET_EXTENSIONS = new Set([
@@ -29161,12 +29237,12 @@ function projectStructure(projectPath) {
29161
29237
  if (steppedOver(directory)) {
29162
29238
  return;
29163
29239
  }
29164
- for (const entry of readdirSync3(directory, { withFileTypes: true })) {
29240
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
29165
29241
  if (skipped(entry.name)) {
29166
29242
  continue;
29167
29243
  }
29168
29244
  if (entry.isDirectory()) {
29169
- visit(join12(directory, entry.name));
29245
+ visit(join13(directory, entry.name));
29170
29246
  } else if (entry.isFile()) {
29171
29247
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
29172
29248
  if (SCENE_EXTENSIONS.has(extension)) {
@@ -29190,13 +29266,13 @@ function projectFiles(projectPath, extensions) {
29190
29266
  if (steppedOver(directory)) {
29191
29267
  return;
29192
29268
  }
29193
- for (const entry of readdirSync3(directory, { withFileTypes: true })) {
29269
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
29194
29270
  if (skipped(entry.name)) {
29195
29271
  continue;
29196
29272
  }
29197
29273
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
29198
29274
  if (entry.isDirectory()) {
29199
- visit(join12(directory, entry.name), spelled);
29275
+ visit(join13(directory, entry.name), spelled);
29200
29276
  } else if (entry.isFile() && extensions.includes(extname(entry.name).toLowerCase())) {
29201
29277
  found.push(spelled);
29202
29278
  }
@@ -29212,20 +29288,20 @@ function scriptsWithoutUid(projectPath) {
29212
29288
  if (steppedOver(directory)) {
29213
29289
  return;
29214
29290
  }
29215
- for (const entry of readdirSync3(directory, { withFileTypes: true })) {
29291
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
29216
29292
  if (skipped(entry.name)) {
29217
29293
  continue;
29218
29294
  }
29219
29295
  const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
29220
29296
  if (entry.isDirectory()) {
29221
- visit(join12(directory, entry.name), spelled);
29297
+ visit(join13(directory, entry.name), spelled);
29222
29298
  continue;
29223
29299
  }
29224
29300
  if (!entry.isFile()) {
29225
29301
  continue;
29226
29302
  }
29227
29303
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
29228
- if (CARRIES_A_UID.has(extension) && !existsSync8(join12(directory, `${entry.name}.uid`))) {
29304
+ if (CARRIES_A_UID.has(extension) && !existsSync9(join13(directory, `${entry.name}.uid`))) {
29229
29305
  found.push(spelled);
29230
29306
  }
29231
29307
  }
@@ -29253,11 +29329,11 @@ function projectTexts(projectPath) {
29253
29329
  if (steppedOver(directory)) {
29254
29330
  return;
29255
29331
  }
29256
- for (const entry of readdirSync3(directory, { withFileTypes: true })) {
29332
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
29257
29333
  if (skipped(entry.name)) {
29258
29334
  continue;
29259
29335
  }
29260
- const absolute = join12(directory, entry.name);
29336
+ const absolute = join13(directory, entry.name);
29261
29337
  const spelled = `${prefix}${entry.name}`;
29262
29338
  if (entry.isDirectory()) {
29263
29339
  visit(absolute, `${spelled}/`);
@@ -29283,11 +29359,11 @@ function ignoredTexts(projectPath) {
29283
29359
  return;
29284
29360
  }
29285
29361
  const under = ignored || gdignored(directory);
29286
- for (const entry of readdirSync3(directory, { withFileTypes: true })) {
29362
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
29287
29363
  if (skipped(entry.name)) {
29288
29364
  continue;
29289
29365
  }
29290
- const absolute = join12(directory, entry.name);
29366
+ const absolute = join13(directory, entry.name);
29291
29367
  const spelled = `${prefix}${entry.name}`;
29292
29368
  if (entry.isDirectory()) {
29293
29369
  visit(absolute, `${spelled}/`, under);
@@ -29320,14 +29396,14 @@ function searchProject(projectPath, options) {
29320
29396
  if (steppedOver(directory)) {
29321
29397
  return;
29322
29398
  }
29323
- for (const entry of readdirSync3(directory, { withFileTypes: true })) {
29399
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
29324
29400
  if (full()) {
29325
29401
  return;
29326
29402
  }
29327
29403
  if (skipped(entry.name)) {
29328
29404
  continue;
29329
29405
  }
29330
- const entryPath = join12(directory, entry.name);
29406
+ const entryPath = join13(directory, entry.name);
29331
29407
  if (entry.isDirectory()) {
29332
29408
  visit(entryPath);
29333
29409
  continue;
@@ -29366,8 +29442,8 @@ function searchProject(projectPath, options) {
29366
29442
  }
29367
29443
 
29368
29444
  // src/rename.ts
29369
- import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync14, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync9 } from "node:fs";
29370
- import { dirname as dirname8, posix as posix2 } from "node:path";
29445
+ import { existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync14, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync9 } from "node:fs";
29446
+ import { dirname as dirname9, posix as posix2 } from "node:path";
29371
29447
  class Script {
29372
29448
  path;
29373
29449
  text;
@@ -30096,21 +30172,21 @@ function applyRewrites(rewrites) {
30096
30172
  if (now !== rewrite.before) {
30097
30173
  throw new Error(`${rewrite.path} changed on disk while the rename was being worked out; nothing was written`);
30098
30174
  }
30099
- if (rewrite.writeTo !== rewrite.absolute && existsSync9(rewrite.writeTo)) {
30175
+ if (rewrite.writeTo !== rewrite.absolute && existsSync10(rewrite.writeTo)) {
30100
30176
  throw new Error(`${rewrite.writeTo} already exists; nothing was written`);
30101
30177
  }
30102
30178
  }
30103
30179
  const done = [];
30104
30180
  try {
30105
30181
  for (const rewrite of rewrites) {
30106
- mkdirSync8(dirname8(rewrite.writeTo), { recursive: true });
30182
+ mkdirSync8(dirname9(rewrite.writeTo), { recursive: true });
30107
30183
  const staging = `${rewrite.writeTo}.gdharness-rename`;
30108
30184
  writeFileSync9(staging, rewrite.after, "utf8");
30109
30185
  renameSync2(staging, rewrite.writeTo);
30110
30186
  let movedUid = false;
30111
30187
  if (rewrite.writeTo !== rewrite.absolute) {
30112
30188
  unlinkSync2(rewrite.absolute);
30113
- if (existsSync9(`${rewrite.absolute}.uid`)) {
30189
+ if (existsSync10(`${rewrite.absolute}.uid`)) {
30114
30190
  renameSync2(`${rewrite.absolute}.uid`, `${rewrite.writeTo}.uid`);
30115
30191
  movedUid = true;
30116
30192
  }
@@ -30134,14 +30210,14 @@ function applyRewrites(rewrites) {
30134
30210
  }
30135
30211
 
30136
30212
  // src/rename-symbol.ts
30137
- import { existsSync as existsSync11, mkdirSync as mkdirSync9, readdirSync as readdirSync4, realpathSync as realpathSync4, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "node:fs";
30138
- import { basename as basename2, dirname as dirname10, join as join14, relative as relative3, resolve as resolve5 } from "node:path";
30213
+ import { existsSync as existsSync12, mkdirSync as mkdirSync9, readdirSync as readdirSync5, realpathSync as realpathSync4, rmSync as rmSync7, writeFileSync as writeFileSync10 } from "node:fs";
30214
+ import { basename as basename3, dirname as dirname11, join as join15, relative as relative3, resolve as resolve5 } from "node:path";
30139
30215
 
30140
30216
  // src/lsp_client.ts
30141
- import { existsSync as existsSync10, realpathSync as realpathSync2 } from "node:fs";
30217
+ import { existsSync as existsSync11, realpathSync as realpathSync2 } from "node:fs";
30142
30218
  import { readFile, realpath } from "node:fs/promises";
30143
30219
  import { createConnection as createConnection3 } from "node:net";
30144
- import { dirname as dirname9, join as join13, resolve as resolve3 } from "node:path";
30220
+ import { dirname as dirname10, join as join14, resolve as resolve3 } from "node:path";
30145
30221
  import { setTimeout as delay4 } from "node:timers/promises";
30146
30222
  import { fileURLToPath, pathToFileURL } from "node:url";
30147
30223
  var DEFAULT_LSP_PORT = 6005;
@@ -30339,7 +30415,7 @@ class GodotLSPClient {
30339
30415
  if (this.initialized) {
30340
30416
  return;
30341
30417
  }
30342
- const rootPath = this.rootPath ?? dirname9(resolve3(filePath));
30418
+ const rootPath = this.rootPath ?? dirname10(resolve3(filePath));
30343
30419
  await this.initialize(rootPath);
30344
30420
  }
30345
30421
  async sendRequest(method, params, timeoutMs = this.timeouts.requestMs) {
@@ -30912,7 +30988,7 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
30912
30988
  } catch {
30913
30989
  throw new ArgumentRefusal(`Project path does not exist: ${requestedProjectPath}`);
30914
30990
  }
30915
- if (!existsSync10(join13(projectPath, "project.godot"))) {
30991
+ if (!existsSync11(join14(projectPath, "project.godot"))) {
30916
30992
  throw new ArgumentRefusal(`Not a Godot project: ${projectPath}. Point projectPath at the directory holding project.godot.`);
30917
30993
  }
30918
30994
  const contained = resolveWithinProject(projectPath, scriptPathValue);
@@ -31287,13 +31363,13 @@ function onDiskSpelling(path) {
31287
31363
  let at = resolve5(path);
31288
31364
  for (;; ) {
31289
31365
  try {
31290
- return join14(realpathSync4.native(at), ...missing.reverse());
31366
+ return join15(realpathSync4.native(at), ...missing.reverse());
31291
31367
  } catch {
31292
- const parent = dirname10(at);
31368
+ const parent = dirname11(at);
31293
31369
  if (parent === at) {
31294
31370
  return resolve5(path);
31295
31371
  }
31296
- missing.push(basename2(at));
31372
+ missing.push(basename3(at));
31297
31373
  at = parent;
31298
31374
  }
31299
31375
  }
@@ -31340,7 +31416,7 @@ async function renameSymbol(request, services) {
31340
31416
  reason: `newScriptPath must be a .gd path inside the project, and ${request.newScriptPath} is not.`
31341
31417
  };
31342
31418
  }
31343
- if (existsSync11(request.newScriptPath)) {
31419
+ if (existsSync12(request.newScriptPath)) {
31344
31420
  return { ok: false, reason: `${moveTo} already exists, so the script cannot be moved there.` };
31345
31421
  }
31346
31422
  }
@@ -31516,7 +31592,7 @@ async function renameSymbol(request, services) {
31516
31592
  scripts,
31517
31593
  moves: new Map([[declaringPath, moveTo]]),
31518
31594
  named: [declaringPath],
31519
- exists: (path) => existsSync11(`${projectPath}/${path.slice("res://".length)}`)
31595
+ exists: (path) => existsSync12(`${projectPath}/${path.slice("res://".length)}`)
31520
31596
  }, plan);
31521
31597
  }
31522
31598
  plan = mentionIgnored(plan, ignoredTexts(projectPath), symbol, moveTo === null ? [] : [declaringPath]);
@@ -31717,15 +31793,15 @@ ${mention.kind}`;
31717
31793
  var LEFT_ALONE_ANSWER_LINES = 300;
31718
31794
  var detailWrites = 0;
31719
31795
  function writeLeftAloneDetail(projectPath, symbol, detail) {
31720
- const directory = join14(projectPath, ".godot", "gdharness-renames");
31796
+ const directory = join15(projectPath, ".godot", "gdharness-renames");
31721
31797
  mkdirSync9(directory, { recursive: true });
31722
31798
  detailWrites += 1;
31723
- const path = join14(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
31799
+ const path = join15(directory, `${String(Date.now())}-${String(detailWrites).padStart(6, "0")}-${String(process.pid)}-${symbol}.json`);
31724
31800
  writeFileSync10(path, `${JSON.stringify(detail, null, 2)}
31725
31801
  `, "utf8");
31726
- const kept = readdirSync4(directory).filter((name) => name.endsWith(".json")).sort().reverse();
31802
+ const kept = readdirSync5(directory).filter((name) => name.endsWith(".json")).sort().reverse();
31727
31803
  for (const old of kept.slice(10)) {
31728
- rmSync7(join14(directory, old), { force: true });
31804
+ rmSync7(join15(directory, old), { force: true });
31729
31805
  }
31730
31806
  return path;
31731
31807
  }
@@ -31750,15 +31826,15 @@ function summaryOf(edits, mentions, rewrites, left) {
31750
31826
  // src/file-move.ts
31751
31827
  var SIDECARS = [".uid", ".import"];
31752
31828
  function filesUnder(directory) {
31753
- return readdirSync5(directory, { withFileTypes: true }).flatMap((entry) => {
31754
- const path = join15(directory, entry.name);
31829
+ return readdirSync6(directory, { withFileTypes: true }).flatMap((entry) => {
31830
+ const path = join16(directory, entry.name);
31755
31831
  return entry.isDirectory() ? filesUnder(path) : [path];
31756
31832
  });
31757
31833
  }
31758
31834
  function sameFile(one, other) {
31759
31835
  try {
31760
- const a = statSync6(one);
31761
- const b = statSync6(other);
31836
+ const a = statSync7(one);
31837
+ const b = statSync7(other);
31762
31838
  return a.ino === b.ino && a.dev === b.dev;
31763
31839
  } catch {
31764
31840
  return false;
@@ -31772,8 +31848,8 @@ function unseenByTheEngine(projectPath, path) {
31772
31848
  return `${path} is under ${step}, and the engine passes over every directory whose name starts with a dot`;
31773
31849
  }
31774
31850
  if (at < steps.length - 1) {
31775
- directory = join15(directory, step);
31776
- if (existsSync12(join15(directory, ".gdignore"))) {
31851
+ directory = join16(directory, step);
31852
+ if (existsSync13(join16(directory, ".gdignore"))) {
31777
31853
  return `${path} is under a directory holding a .gdignore, which the engine does not load`;
31778
31854
  }
31779
31855
  }
@@ -31785,21 +31861,21 @@ function installedAddonOf(projectPath, path) {
31785
31861
  if (steps[0] !== "addons" || steps[1] === undefined) {
31786
31862
  return null;
31787
31863
  }
31788
- return existsSync12(join15(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
31864
+ return existsSync13(join16(projectPath, "addons", steps[1], VERSION_MARKER)) ? `res://addons/${steps[1]}` : null;
31789
31865
  }
31790
31866
  function binaryResources(projectPath) {
31791
31867
  const found = [];
31792
31868
  const visit = (directory, prefix) => {
31793
- if (existsSync12(join15(directory, ".gdignore"))) {
31869
+ if (existsSync13(join16(directory, ".gdignore"))) {
31794
31870
  return;
31795
31871
  }
31796
- for (const entry of readdirSync5(directory, { withFileTypes: true })) {
31872
+ for (const entry of readdirSync6(directory, { withFileTypes: true })) {
31797
31873
  if (entry.name.startsWith(".")) {
31798
31874
  continue;
31799
31875
  }
31800
31876
  const path = `${prefix}${entry.name}`;
31801
31877
  if (entry.isDirectory()) {
31802
- visit(join15(directory, entry.name), `${path}/`);
31878
+ visit(join16(directory, entry.name), `${path}/`);
31803
31879
  } else if (/\.(scn|res)$/i.test(entry.name)) {
31804
31880
  found.push(path);
31805
31881
  }
@@ -31809,7 +31885,7 @@ function binaryResources(projectPath) {
31809
31885
  return found;
31810
31886
  }
31811
31887
  function targetPathOf(projectPath, absolute) {
31812
- const inside = relative4(onDiskSpelling(projectPath), join15(onDiskSpelling(dirname11(absolute)), basename3(absolute)));
31888
+ const inside = relative4(onDiskSpelling(projectPath), join16(onDiskSpelling(dirname12(absolute)), basename4(absolute)));
31813
31889
  if (inside === "" || inside.startsWith("..") || /^[A-Za-z]:/.test(inside)) {
31814
31890
  return null;
31815
31891
  }
@@ -31827,13 +31903,13 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
31827
31903
  if (to === null) {
31828
31904
  return { ok: false, reason: `newPath must be a path inside the project, and ${absoluteTo} is not.` };
31829
31905
  }
31830
- if (!existsSync12(absoluteFrom)) {
31906
+ if (!existsSync13(absoluteFrom)) {
31831
31907
  return { ok: false, reason: `There is nothing at ${from} to move.` };
31832
31908
  }
31833
31909
  if (from === to) {
31834
31910
  return { ok: false, reason: `${from} is already there; nothing to move.` };
31835
31911
  }
31836
- const isDirectory = statSync6(absoluteFrom).isDirectory();
31912
+ const isDirectory = statSync7(absoluteFrom).isDirectory();
31837
31913
  if (from === "res://project.godot") {
31838
31914
  return {
31839
31915
  ok: false,
@@ -31856,7 +31932,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
31856
31932
  if (isDirectory && to.startsWith(`${from}/`)) {
31857
31933
  return { ok: false, reason: `${from} cannot be moved into itself.` };
31858
31934
  }
31859
- if (existsSync12(absoluteTo) && !sameFile(absoluteFrom, absoluteTo)) {
31935
+ if (existsSync13(absoluteTo) && !sameFile(absoluteFrom, absoluteTo)) {
31860
31936
  return { ok: false, reason: `${to} already exists, so nothing was moved there.` };
31861
31937
  }
31862
31938
  const unseen = unseenByTheEngine(projectPath, to);
@@ -31864,7 +31940,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
31864
31940
  return { ok: false, reason: `${unseen}, so nothing there could load what was moved.` };
31865
31941
  }
31866
31942
  if (!isDirectory) {
31867
- const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync12(join15(projectPath, path.slice("res://".length))));
31943
+ const owner = SIDECARS.map((sidecar) => from.endsWith(sidecar) ? from.slice(0, -sidecar.length) : null).find((path) => path !== null && existsSync13(join16(projectPath, path.slice("res://".length))));
31868
31944
  if (owner !== undefined && owner !== null) {
31869
31945
  return { ok: false, reason: `${from} belongs to ${owner} and moves with it: move ${owner} instead.` };
31870
31946
  }
@@ -31886,7 +31962,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
31886
31962
  } else {
31887
31963
  moves.set(from, to);
31888
31964
  for (const sidecar of SIDECARS) {
31889
- if (existsSync12(`${absoluteFrom}${sidecar}`)) {
31965
+ if (existsSync13(`${absoluteFrom}${sidecar}`)) {
31890
31966
  moves.set(`${from}${sidecar}`, `${to}${sidecar}`);
31891
31967
  }
31892
31968
  }
@@ -31898,12 +31974,12 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
31898
31974
  scripts,
31899
31975
  moves,
31900
31976
  named: [from],
31901
- exists: (path) => existsSync12(join15(projectPath, path.slice("res://".length)))
31977
+ exists: (path) => existsSync13(join16(projectPath, path.slice("res://".length)))
31902
31978
  }, { edits: [], mentions: [] });
31903
31979
  plan = mentionIgnored(plan, ignoredTexts(projectPath), null, [from]);
31904
31980
  const binaries = [];
31905
31981
  for (const path of binaryResources(projectPath)) {
31906
- const bytes = readFileSync16(join15(projectPath, path.slice("res://".length)));
31982
+ const bytes = readFileSync16(join16(projectPath, path.slice("res://".length)));
31907
31983
  const names = [...moves.keys()].find((moving) => bytes.includes(moving));
31908
31984
  if (names !== undefined) {
31909
31985
  binaries.push({
@@ -31922,7 +31998,7 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
31922
31998
  }));
31923
31999
  const left = leftAloneReport(mentions);
31924
32000
  const rewrites = rewritesOf(plan, texts, null);
31925
- const absoluteOf = (path) => join15(projectPath, path.slice("res://".length));
32001
+ const absoluteOf = (path) => join16(projectPath, path.slice("res://".length));
31926
32002
  const rewritten = new Map(rewrites.map((rewrite) => [rewrite.path, rewrite]));
31927
32003
  const moved = [...moves].map(([source, target]) => {
31928
32004
  const rewrite = rewritten.get(source);
@@ -31969,19 +32045,19 @@ function applyFileMove(outcome) {
31969
32045
  }
31970
32046
  }
31971
32047
  for (const file of outcome.moved) {
31972
- if (!existsSync12(file.absoluteFrom)) {
32048
+ if (!existsSync13(file.absoluteFrom)) {
31973
32049
  throw new Error(`${file.from} is gone from disk; nothing was written`);
31974
32050
  }
31975
32051
  if (file.text !== null && readFileSync16(file.absoluteFrom, "utf8") !== file.text.before) {
31976
32052
  throw new Error(`${file.from} changed on disk while the move was being worked out; nothing was written`);
31977
32053
  }
31978
- if (existsSync12(file.absoluteTo) && !sameFile(file.absoluteFrom, file.absoluteTo)) {
32054
+ if (existsSync13(file.absoluteTo) && !sameFile(file.absoluteFrom, file.absoluteTo)) {
31979
32055
  throw new Error(`${file.to} already exists; nothing was written`);
31980
32056
  }
31981
32057
  }
31982
32058
  const undo = [];
31983
32059
  const rename = (source, target) => {
31984
- mkdirSync10(dirname11(target), { recursive: true });
32060
+ mkdirSync10(dirname12(target), { recursive: true });
31985
32061
  renameSync3(source, target);
31986
32062
  undo.push(() => {
31987
32063
  renameSync3(target, source);
@@ -32704,27 +32780,27 @@ function getDefaultBridge(ownProject = null) {
32704
32780
  }
32705
32781
 
32706
32782
  // src/godot-path.ts
32707
- import { existsSync as existsSync14 } from "node:fs";
32783
+ import { existsSync as existsSync15 } from "node:fs";
32708
32784
  import { normalize } from "node:path";
32709
32785
 
32710
32786
  // src/detection.ts
32711
- import { existsSync as existsSync13, readdirSync as readdirSync6, statSync as statSync7 } from "node:fs";
32712
- import { homedir as homedir2 } from "node:os";
32713
- import { join as join16 } from "node:path";
32787
+ import { existsSync as existsSync14, readdirSync as readdirSync7, statSync as statSync8 } from "node:fs";
32788
+ import { homedir as homedir3 } from "node:os";
32789
+ import { join as join17 } from "node:path";
32714
32790
  function resolveHomeDirectory() {
32715
32791
  try {
32716
- return homedir2();
32792
+ return homedir3();
32717
32793
  } catch {
32718
32794
  return "";
32719
32795
  }
32720
32796
  }
32721
32797
  function scanDirectoryForGodotBinaries(directory, platform) {
32722
- if (!directory || !existsSync13(directory)) {
32798
+ if (!directory || !existsSync14(directory)) {
32723
32799
  return [];
32724
32800
  }
32725
32801
  let entries;
32726
32802
  try {
32727
- entries = readdirSync6(directory);
32803
+ entries = readdirSync7(directory);
32728
32804
  } catch {
32729
32805
  return [];
32730
32806
  }
@@ -32734,16 +32810,16 @@ function scanDirectoryForGodotBinaries(directory, platform) {
32734
32810
  if (!pattern.test(name)) {
32735
32811
  continue;
32736
32812
  }
32737
- const fullPath = join16(directory, name);
32813
+ const fullPath = join17(directory, name);
32738
32814
  try {
32739
- const stat = statSync7(fullPath);
32815
+ const stat = statSync8(fullPath);
32740
32816
  if (stat.isFile()) {
32741
32817
  matches.push({ name, mtime: stat.mtimeMs });
32742
32818
  }
32743
32819
  } catch {}
32744
32820
  }
32745
32821
  matches.sort((a, b) => b.mtime - a.mtime);
32746
- return matches.map((m) => join16(directory, m.name));
32822
+ return matches.map((m) => join17(directory, m.name));
32747
32823
  }
32748
32824
  function conventionalPaths(platform, home) {
32749
32825
  const paths = ["godot"];
@@ -32844,7 +32920,7 @@ class GodotLocator {
32844
32920
  return known;
32845
32921
  }
32846
32922
  let reason;
32847
- if (path !== "godot" && !existsSync14(path)) {
32923
+ if (path !== "godot" && !existsSync15(path)) {
32848
32924
  reason = "does not exist";
32849
32925
  } else {
32850
32926
  try {
@@ -32860,12 +32936,12 @@ class GodotLocator {
32860
32936
  }
32861
32937
 
32862
32938
  // src/headless.ts
32863
- import { existsSync as existsSync18, readFileSync as readFileSync19, realpathSync as realpathSync5, writeFileSync as writeFileSync13 } from "node:fs";
32864
- import { dirname as dirname13, join as join20 } from "node:path";
32939
+ import { existsSync as existsSync19, readFileSync as readFileSync19, realpathSync as realpathSync5, writeFileSync as writeFileSync13 } from "node:fs";
32940
+ import { dirname as dirname14, join as join21 } from "node:path";
32865
32941
 
32866
32942
  // src/library-copies.ts
32867
- import { existsSync as existsSync15, readdirSync as readdirSync7, readFileSync as readFileSync17, renameSync as renameSync4, unlinkSync as unlinkSync3 } from "node:fs";
32868
- import { basename as basename4, dirname as dirname12, isAbsolute as isAbsolute3, join as join17 } from "node:path";
32943
+ import { existsSync as existsSync16, readdirSync as readdirSync8, readFileSync as readFileSync17, renameSync as renameSync4, unlinkSync as unlinkSync3 } from "node:fs";
32944
+ import { basename as basename5, dirname as dirname13, isAbsolute as isAbsolute4, join as join18 } from "node:path";
32869
32945
  function freeLibraryCopies(projectPath, platform = process.platform) {
32870
32946
  const setAside = [];
32871
32947
  const swept = [];
@@ -32873,26 +32949,26 @@ function freeLibraryCopies(projectPath, platform = process.platform) {
32873
32949
  return { setAside, swept };
32874
32950
  }
32875
32951
  for (const library of windowsLibraries(projectPath)) {
32876
- const folder = dirname12(library);
32877
- const copy = `~${basename4(library)}`;
32878
- if (existsSync15(join17(folder, copy))) {
32879
- const aside = join17(folder, `${copy}.aside-${process.pid}-${Date.now()}`);
32952
+ const folder = dirname13(library);
32953
+ const copy = `~${basename5(library)}`;
32954
+ if (existsSync16(join18(folder, copy))) {
32955
+ const aside = join18(folder, `${copy}.aside-${process.pid}-${Date.now()}`);
32880
32956
  try {
32881
- renameSync4(join17(folder, copy), aside);
32957
+ renameSync4(join18(folder, copy), aside);
32882
32958
  setAside.push(aside);
32883
32959
  } catch {}
32884
32960
  }
32885
32961
  let names;
32886
32962
  try {
32887
- names = readdirSync7(folder);
32963
+ names = readdirSync8(folder);
32888
32964
  } catch {
32889
32965
  continue;
32890
32966
  }
32891
32967
  for (const name of names) {
32892
32968
  if (name.length > copy.length && name.toLowerCase().startsWith(copy.toLowerCase())) {
32893
32969
  try {
32894
- unlinkSync3(join17(folder, name));
32895
- swept.push(join17(folder, name));
32970
+ unlinkSync3(join18(folder, name));
32971
+ swept.push(join18(folder, name));
32896
32972
  } catch {}
32897
32973
  }
32898
32974
  }
@@ -32900,7 +32976,7 @@ function freeLibraryCopies(projectPath, platform = process.platform) {
32900
32976
  return { setAside, swept };
32901
32977
  }
32902
32978
  function windowsLibraries(projectPath) {
32903
- const listed = readOrNull(join17(projectPath, ".godot", "extension_list.cfg"));
32979
+ const listed = readOrNull(join18(projectPath, ".godot", "extension_list.cfg"));
32904
32980
  if (listed === null) {
32905
32981
  return [];
32906
32982
  }
@@ -32919,7 +32995,7 @@ function windowsLibraries(projectPath) {
32919
32995
  continue;
32920
32996
  }
32921
32997
  const named = inLibraries ? /^\s*windows[\w.]*\s*=\s*"([^"]+)"/.exec(row)?.[1] : undefined;
32922
- const library = named === undefined ? null : resolveResource(projectPath, dirname12(manifest), named);
32998
+ const library = named === undefined ? null : resolveResource(projectPath, dirname13(manifest), named);
32923
32999
  if (library !== null) {
32924
33000
  libraries.add(library);
32925
33001
  }
@@ -32932,9 +33008,9 @@ function resolveResource(projectPath, relativeTo, path) {
32932
33008
  return null;
32933
33009
  }
32934
33010
  if (path.startsWith("res://")) {
32935
- return join17(projectPath, path.slice("res://".length));
33011
+ return join18(projectPath, path.slice("res://".length));
32936
33012
  }
32937
- return isAbsolute3(path) ? path : join17(relativeTo, path);
33013
+ return isAbsolute4(path) ? path : join18(relativeTo, path);
32938
33014
  }
32939
33015
  function readOrNull(path) {
32940
33016
  try {
@@ -32946,34 +33022,35 @@ function readOrNull(path) {
32946
33022
 
32947
33023
  // src/reimport.ts
32948
33024
  import { createHash as createHash3 } from "node:crypto";
32949
- import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as rmSync8, statSync as statSync8, utimesSync, writeFileSync as writeFileSync12 } from "node:fs";
32950
- import { basename as basename5, join as join18 } from "node:path";
33025
+ import { existsSync as existsSync17, readFileSync as readFileSync18, rmSync as rmSync8, statSync as statSync9, utimesSync, writeFileSync as writeFileSync12 } from "node:fs";
33026
+ import { basename as basename6, join as join19 } from "node:path";
32951
33027
  function forceNextImport(projectPath, resourcePath) {
32952
33028
  const relative = resourcePath.replace(/^res:\/\//, "");
32953
- const sidecar = join18(projectPath, `${relative}.import`);
32954
- if (!existsSync16(sidecar)) {
33029
+ const sidecar = join19(projectPath, `${relative}.import`);
33030
+ if (!existsSync17(sidecar)) {
32955
33031
  return;
32956
33032
  }
32957
- const { atime, mtimeMs } = statSync8(sidecar);
33033
+ const { atime, mtimeMs } = statSync9(sidecar);
32958
33034
  writeFileSync12(sidecar, readFileSync18(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
32959
33035
  utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
32960
33036
  rmSync8(importRecord(projectPath, resourcePath), { force: true });
32961
33037
  }
32962
33038
  function importRecord(projectPath, resourcePath) {
32963
33039
  const digest = createHash3("md5").update(resourcePath).digest("hex");
32964
- return join18(projectPath, ".godot", "imported", `${basename5(resourcePath)}-${digest}.md5`);
33040
+ return join19(projectPath, ".godot", "imported", `${basename6(resourcePath)}-${digest}.md5`);
32965
33041
  }
32966
33042
 
32967
33043
  // src/scratch.ts
32968
- import { existsSync as existsSync17, mkdtempSync as mkdtempSync2, readdirSync as readdirSync8, rmSync as rmSync9 } from "node:fs";
33044
+ import { existsSync as existsSync18, mkdtempSync as mkdtempSync2, readdirSync as readdirSync9, rmSync as rmSync9 } from "node:fs";
32969
33045
  import { tmpdir as tmpdir5 } from "node:os";
32970
- import { join as join19 } from "node:path";
32971
- import process7 from "node:process";
33046
+ import { join as join20 } from "node:path";
33047
+ import process8 from "node:process";
32972
33048
  var SWEPT = { recursive: true, force: true };
32973
33049
  var AGAIN_AFTER_MS = [250, 500, 1000, 2000, 4000, 8000, 16000];
32974
33050
  var waiting = new Set;
32975
33051
  var atExit = new Set;
32976
33052
  var KINDS = [
33053
+ "answers",
32977
33054
  "desktop",
32978
33055
  "export",
32979
33056
  "import",
@@ -32982,12 +33059,12 @@ var KINDS = [
32982
33059
  "tests"
32983
33060
  ];
32984
33061
  function scratchDirectory(kind) {
32985
- return mkdtempSync2(join19(tmpdir5(), `gdharness-${kind}-${process7.pid}-`));
33062
+ return mkdtempSync2(join20(tmpdir5(), `gdharness-${kind}-${process8.pid}-`));
32986
33063
  }
32987
33064
  function sweepAbandonedScratch(directory = tmpdir5()) {
32988
33065
  let entries;
32989
33066
  try {
32990
- entries = readdirSync8(directory);
33067
+ entries = readdirSync9(directory);
32991
33068
  } catch {
32992
33069
  return;
32993
33070
  }
@@ -32998,7 +33075,7 @@ function sweepAbandonedScratch(directory = tmpdir5()) {
32998
33075
  continue;
32999
33076
  }
33000
33077
  try {
33001
- remove(join19(directory, entry));
33078
+ remove(join20(directory, entry));
33002
33079
  } catch {}
33003
33080
  }
33004
33081
  }
@@ -33036,7 +33113,7 @@ function tryAgain(path, attempt) {
33036
33113
  lastTryOnExit();
33037
33114
  }
33038
33115
  const after = AGAIN_AFTER_MS[attempt];
33039
- if (after === undefined || !existsSync17(path)) {
33116
+ if (after === undefined || !existsSync18(path)) {
33040
33117
  waiting.delete(path);
33041
33118
  if (after === undefined) {
33042
33119
  atExit.add(path);
@@ -33058,7 +33135,7 @@ function lastTryOnExit() {
33058
33135
  return;
33059
33136
  }
33060
33137
  lastTryWatched = true;
33061
- process7.once("exit", () => {
33138
+ process8.once("exit", () => {
33062
33139
  for (const path of [...waiting, ...atExit]) {
33063
33140
  try {
33064
33141
  remove(path);
@@ -33078,8 +33155,14 @@ function snakeCased(params) {
33078
33155
  }
33079
33156
  function engineExtras(outcome) {
33080
33157
  const after = outcome.afterAnswerMessages ?? [];
33158
+ const before = outcome.beforeOperationMessages ?? [];
33081
33159
  return {
33082
33160
  ...outcome.messages.length > 0 ? { engine_messages: outcome.messages } : {},
33161
+ ...before.length > 0 ? {
33162
+ engine_messages_before_operation: before,
33163
+ engine_messages_before_operation_note: BEFORE_OPERATION_NOTE
33164
+ } : {},
33165
+ ...outcome.beforeOperationOmitted === undefined ? {} : { engine_messages_before_operation_omitted: outcome.beforeOperationOmitted },
33083
33166
  ...outcome.messagesOmitted === undefined ? {} : { engine_messages_omitted: outcome.messagesOmitted },
33084
33167
  ...after.length > 0 ? { engine_messages_after_answer: after, engine_messages_after_answer_note: AFTER_ANSWER_NOTE } : {},
33085
33168
  ...outcome.afterAnswerOmitted === undefined ? {} : { engine_messages_after_answer_omitted: outcome.afterAnswerOmitted },
@@ -33087,8 +33170,11 @@ function engineExtras(outcome) {
33087
33170
  };
33088
33171
  }
33089
33172
  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.";
33173
+ var 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.";
33090
33174
  var OWN_ERROR = "[ERROR] ";
33091
33175
  var ANSWERED = "[gdharness] answered";
33176
+ var WAITING = "[gdharness] waiting for the autoloads";
33177
+ var BEGUN = "[gdharness] begun";
33092
33178
  var MESSAGES_KEPT = 200;
33093
33179
  var ERRORS_NAMED = 3;
33094
33180
  function problemsIn(log) {
@@ -33127,7 +33213,7 @@ function refusalsIn(log) {
33127
33213
  }
33128
33214
  function scriptFault(problems, script) {
33129
33215
  const spelled = (path) => `(${path.replaceAll("\\", "/").toLowerCase()}/`;
33130
- const directories = [...new Set([spelled(dirname13(script)), spelled(resolvedOr(dirname13(script)))])];
33216
+ const directories = [...new Set([spelled(dirname14(script)), spelled(resolvedOr(dirname14(script)))])];
33131
33217
  return problems.find((entry) => {
33132
33218
  const at = entry.detail.find((line) => line.startsWith("at: "))?.replaceAll("\\", "/").toLowerCase();
33133
33219
  return entry.severity === "error" && at !== undefined && directories.some((directory) => at.includes(directory));
@@ -33165,26 +33251,33 @@ async function runImport(godotPath, projectPath, options = {}) {
33165
33251
  };
33166
33252
  const first = await pass();
33167
33253
  const collided = librariesNotCopied(first.messages);
33168
- if (collided.length === 0) {
33254
+ const crash = first.docCacheNote;
33255
+ if (collided.length === 0 && crash === undefined) {
33169
33256
  return first;
33170
33257
  }
33171
- const second = await pass();
33258
+ const { docCacheNote: _, ...again } = await pass();
33259
+ const second = again.ok || crash === undefined ? again : { ...again, message: `${again.message.replace(/[.\s]+$/, "")}. ${crash}` };
33172
33260
  const still = librariesNotCopied(second.messages);
33173
33261
  return {
33174
33262
  ...second,
33175
- librariesRetried: collided,
33176
- ...still.length === 0 ? {} : { librariesNotLoaded: still },
33177
- extensionNote: extensionRetryNote(collided, still)
33263
+ ...collided.length === 0 ? {} : {
33264
+ librariesRetried: collided,
33265
+ ...still.length === 0 ? {} : { librariesNotLoaded: still },
33266
+ extensionNote: extensionRetryNote(collided, still)
33267
+ },
33268
+ ...crash === undefined ? {} : { docCacheNote: crash }
33178
33269
  };
33179
33270
  }
33180
33271
  async function importOnce(godotPath, projectPath) {
33181
33272
  const logDir = scratchDirectory("import");
33273
+ const caches = editorCacheDirectory(godotPath);
33274
+ const cachesBefore = docCachesWritten(caches);
33182
33275
  let ran;
33183
33276
  try {
33184
33277
  ran = await runEngine(godotPath, [
33185
33278
  "--headless",
33186
33279
  "--log-file",
33187
- join20(logDir, "engine.log"),
33280
+ join21(logDir, "engine.log"),
33188
33281
  "--path",
33189
33282
  projectPath,
33190
33283
  "--import"
@@ -33193,24 +33286,26 @@ async function importOnce(godotPath, projectPath) {
33193
33286
  discard(logDir);
33194
33287
  }
33195
33288
  const problems = problemsIn(ran.log);
33289
+ const crash = docCacheCrash(ran, cachesBefore, docCachesWritten(caches), "import");
33196
33290
  if (ran.exitCode === 0 && ran.failure === null) {
33197
- return { ok: true, ...reported2(problems) };
33291
+ return { ok: true, ...reported2(problems), ...crash === null ? {} : { docCacheNote: crash } };
33198
33292
  }
33199
33293
  return {
33200
33294
  ok: false,
33201
33295
  message: `the import pass failed (${howItEnded(ran)}): ${reason(ran.log, problems)}`,
33202
- ...reported2(problems)
33296
+ ...reported2(problems),
33297
+ ...crash === null ? {} : { docCacheNote: crash }
33203
33298
  };
33204
33299
  }
33205
33300
  async function runOperation(engine, operation, params, projectPath) {
33206
33301
  const paramsDir = scratchDirectory("params");
33207
- const paramsFile = join20(paramsDir, `${operation}.json`);
33208
- const answerFile = join20(paramsDir, "answer.json");
33302
+ const paramsFile = join21(paramsDir, `${operation}.json`);
33303
+ const answerFile = join21(paramsDir, "answer.json");
33209
33304
  writeFileSync13(paramsFile, JSON.stringify(snakeCased(params)), "utf8");
33210
33305
  const args = [
33211
33306
  "--headless",
33212
33307
  "--log-file",
33213
- join20(paramsDir, "engine.log"),
33308
+ join21(paramsDir, "engine.log"),
33214
33309
  "--path",
33215
33310
  projectPath,
33216
33311
  "--script",
@@ -33229,12 +33324,22 @@ async function runOperation(engine, operation, params, projectPath) {
33229
33324
  discard(paramsDir);
33230
33325
  }
33231
33326
  const everything = ran.log.everything();
33232
- const answered = everything.findIndex((entry) => entry.source === "stderr" && entry.text === ANSWERED);
33233
- const problems = problemsAmong(answered === -1 ? everything : everything.slice(0, answered));
33327
+ const marked = (text) => everything.findIndex((entry) => entry.source === "stderr" && entry.text === text);
33328
+ const answered = marked(ANSWERED);
33329
+ const waiting = marked(WAITING);
33330
+ const begun = marked(BEGUN);
33331
+ const ended = answered === -1 ? everything.length : answered;
33332
+ const problems = waiting === -1 ? problemsAmong(everything.slice(0, ended)) : problemsAmong([
33333
+ ...everything.slice(0, waiting),
33334
+ ...begun === -1 ? [] : everything.slice(begun + 1, ended)
33335
+ ]);
33336
+ const startup = waiting === -1 ? [] : problemsAmong(everything.slice(waiting + 1, begun === -1 ? ended : begun));
33234
33337
  const after = answered === -1 ? [] : problemsAmong(everything.slice(answered + 1));
33235
33338
  const afterAnswered = {
33236
33339
  ...after.length === 0 ? {} : { afterAnswerMessages: after.slice(-MESSAGES_KEPT) },
33237
- ...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {}
33340
+ ...after.length > MESSAGES_KEPT ? { afterAnswerOmitted: after.length - MESSAGES_KEPT } : {},
33341
+ ...startup.length === 0 ? {} : { beforeOperationMessages: startup.slice(-MESSAGES_KEPT) },
33342
+ ...startup.length > MESSAGES_KEPT ? { beforeOperationOmitted: startup.length - MESSAGES_KEPT } : {}
33238
33343
  };
33239
33344
  if (answer.kind === "unreadable") {
33240
33345
  return {
@@ -33245,6 +33350,14 @@ async function runOperation(engine, operation, params, projectPath) {
33245
33350
  };
33246
33351
  }
33247
33352
  if (answer.kind === "none") {
33353
+ if (waiting !== -1 && begun === -1) {
33354
+ return {
33355
+ ok: false,
33356
+ 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)}`,
33357
+ ...reported2(problems),
33358
+ ...afterAnswered
33359
+ };
33360
+ }
33248
33361
  const refusals = refusalsIn(ran.log);
33249
33362
  if (refusals.length > 0) {
33250
33363
  const said = new Set(refusals.map((entry) => entry.index));
@@ -33280,7 +33393,7 @@ async function runOperation(engine, operation, params, projectPath) {
33280
33393
  };
33281
33394
  }
33282
33395
  function answerIn(file) {
33283
- if (!existsSync18(file)) {
33396
+ if (!existsSync19(file)) {
33284
33397
  return { kind: "none" };
33285
33398
  }
33286
33399
  const text = readFileSync19(file, "utf8");
@@ -33686,6 +33799,31 @@ ${note}`;
33686
33799
  return detail === entry.detail ? entry : { ...entry, detail };
33687
33800
  });
33688
33801
  }
33802
+ var MOST_REPORTS_SHOWN = 20;
33803
+ var LONGEST_DETAIL = 20000;
33804
+ function shortened(detail) {
33805
+ if (detail === null || detail.length <= LONGEST_DETAIL) {
33806
+ return detail;
33807
+ }
33808
+ return `${detail.slice(0, LONGEST_DETAIL)}
33809
+ [${detail.length - LONGEST_DETAIL} more characters left out]`;
33810
+ }
33811
+ function foldedReports(reports) {
33812
+ const first = new Map;
33813
+ for (const report of reports) {
33814
+ const key = JSON.stringify([report.kind, report.message, report.detail]);
33815
+ const seen = first.get(key);
33816
+ if (seen === undefined) {
33817
+ first.set(key, { report, times: 1 });
33818
+ } else {
33819
+ seen.times += 1;
33820
+ }
33821
+ }
33822
+ return [...first.values()].map(({ report, times }) => {
33823
+ const kept = { ...report, detail: shortened(report.detail) };
33824
+ return times === 1 ? kept : { ...kept, times };
33825
+ });
33826
+ }
33689
33827
  function failedCases(report, printed) {
33690
33828
  const cases = report.suites.flatMap((suite) => suite.cases.filter((entry) => entry.status === "failed" || entry.status === "error").map((entry) => ({ entry, path: suite.path })));
33691
33829
  const read = withActualsPrinted(cases.flatMap(({ entry }) => entry.reports), printed);
@@ -33693,6 +33831,8 @@ function failedCases(report, printed) {
33693
33831
  return cases.map(({ entry, path }) => {
33694
33832
  const reports = read.slice(next, next + entry.reports.length);
33695
33833
  next += entry.reports.length;
33834
+ const folded = foldedReports(reports);
33835
+ const shown = folded.slice(0, MOST_REPORTS_SHOWN);
33696
33836
  return {
33697
33837
  suite: entry.suite,
33698
33838
  name: entry.name,
@@ -33700,11 +33840,34 @@ function failedCases(report, printed) {
33700
33840
  time: entry.time,
33701
33841
  path,
33702
33842
  message: reports[0]?.message ?? null,
33703
- detail: reports[0]?.detail ?? null,
33704
- ...reports.length > 1 ? { reports } : {}
33843
+ detail: shortened(reports[0]?.detail ?? null),
33844
+ ...reports.length > 1 ? { reports: shown, reportCount: reports.length } : {},
33845
+ ...folded.length > shown.length ? { reportsLeftOut: folded.length - shown.length } : {}
33705
33846
  };
33706
33847
  });
33707
33848
  }
33849
+ function failedWithin(failed, budget) {
33850
+ const sizes = failed.map((entry) => JSON.stringify(entry).length);
33851
+ const smallestFirst = sizes.map((_, index) => index).sort((a, b) => (sizes[a] ?? 0) - (sizes[b] ?? 0));
33852
+ const whole = new Set;
33853
+ let spent = 0;
33854
+ for (const index of smallestFirst) {
33855
+ const size = sizes[index] ?? 0;
33856
+ if (spent + size > budget) {
33857
+ break;
33858
+ }
33859
+ spent += size;
33860
+ whole.add(index);
33861
+ }
33862
+ const kept = failed.map((entry, index) => {
33863
+ if (whole.has(index)) {
33864
+ return entry;
33865
+ }
33866
+ const { detail: _detail, reports: _reports, reportsLeftOut: _left, ...named } = entry;
33867
+ return { ...named, message: entry.message?.slice(0, 500) ?? null, detail: null };
33868
+ });
33869
+ return { failed: kept, briefed: failed.length - whole.size };
33870
+ }
33708
33871
  var MASKED_BRACKET = "[lb]";
33709
33872
  function masked(expected, actual) {
33710
33873
  const allSpelled = (value) => !PRINTED_DICTIONARY.test(value) && value.split("[").length === value.split(MASKED_BRACKET).length;
@@ -33856,26 +34019,26 @@ function askTheKeeper(address, line, withinMs) {
33856
34019
  }
33857
34020
 
33858
34021
  // src/lifecycle-log.ts
33859
- import { appendFileSync, mkdirSync as mkdirSync11, renameSync as renameSync5, statSync as statSync9 } from "node:fs";
33860
- import { dirname as dirname14, join as join21 } from "node:path";
33861
- import process8 from "node:process";
34022
+ import { appendFileSync, mkdirSync as mkdirSync11, renameSync as renameSync5, statSync as statSync10 } from "node:fs";
34023
+ import { dirname as dirname15, join as join22 } from "node:path";
34024
+ import process9 from "node:process";
33862
34025
  function lifecycleLogPath() {
33863
- return join21(runtimeDirectory(), "servers.log");
34026
+ return join22(runtimeDirectory(), "servers.log");
33864
34027
  }
33865
34028
  var LIFECYCLE_LOG_LIMIT = 1e6;
33866
34029
  function writeLifecycle(event, fields = {}) {
33867
34030
  const path = lifecycleLogPath();
33868
34031
  try {
33869
- mkdirSync11(dirname14(path), { recursive: true });
33870
- if ((statSync9(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
34032
+ mkdirSync11(dirname15(path), { recursive: true });
34033
+ if ((statSync10(path, { throwIfNoEntry: false })?.size ?? 0) > LIFECYCLE_LOG_LIMIT) {
33871
34034
  try {
33872
34035
  renameSync5(path, `${path}.1`);
33873
34036
  } catch {}
33874
34037
  }
33875
34038
  const line = {
33876
34039
  at: new Date().toISOString(),
33877
- pid: process8.pid,
33878
- ppid: process8.ppid,
34040
+ pid: process9.pid,
34041
+ ppid: process9.ppid,
33879
34042
  version: SERVER_VERSION,
33880
34043
  event
33881
34044
  };
@@ -34164,14 +34327,14 @@ function setupPromptHandlers(server) {
34164
34327
 
34165
34328
  // src/restart-note.ts
34166
34329
  import { mkdirSync as mkdirSync12, readFileSync as readFileSync20, rmSync as rmSync10, writeFileSync as writeFileSync14 } from "node:fs";
34167
- import { dirname as dirname15, join as join22 } from "node:path";
34330
+ import { dirname as dirname16, join as join23 } from "node:path";
34168
34331
  function restartNotePath(projectPath) {
34169
- return join22(projectPath, ".godot", "gdharness-restart.json");
34332
+ return join23(projectPath, ".godot", "gdharness-restart.json");
34170
34333
  }
34171
34334
  function noteRestartBegun(note) {
34172
34335
  const path = restartNotePath(note.projectPath);
34173
34336
  try {
34174
- mkdirSync12(dirname15(path), { recursive: true });
34337
+ mkdirSync12(dirname16(path), { recursive: true });
34175
34338
  writeFileSync14(path, `${JSON.stringify(note, null, 2)}
34176
34339
  `, "utf8");
34177
34340
  } catch (error) {
@@ -34203,23 +34366,23 @@ function restartSettled(projectPath) {
34203
34366
 
34204
34367
  // src/run-record.ts
34205
34368
  import { createHash as createHash4 } from "node:crypto";
34206
- import { mkdirSync as mkdirSync13, openSync as openSync3, readdirSync as readdirSync9, readFileSync as readFileSync21, realpathSync as realpathSync6, rmSync as rmSync11, statSync as statSync10 } from "node:fs";
34207
- import { basename as basename6, dirname as dirname16, join as join23, resolve as resolve6 } from "node:path";
34369
+ import { mkdirSync as mkdirSync13, openSync as openSync3, readdirSync as readdirSync10, readFileSync as readFileSync21, realpathSync as realpathSync6, rmSync as rmSync11, statSync as statSync11 } from "node:fs";
34370
+ import { basename as basename7, dirname as dirname17, join as join24, resolve as resolve6 } from "node:path";
34208
34371
  function runsDirectory() {
34209
- return join23(runtimeDirectory(), "runs");
34372
+ return join24(runtimeDirectory(), "runs");
34210
34373
  }
34211
34374
  function realAsFarAsItExists(path) {
34212
34375
  const missing = [];
34213
34376
  let at = path;
34214
34377
  for (;; ) {
34215
34378
  try {
34216
- return join23(realpathSync6.native(at), ...missing);
34379
+ return join24(realpathSync6.native(at), ...missing);
34217
34380
  } catch {
34218
- const parent = dirname16(at);
34381
+ const parent = dirname17(at);
34219
34382
  if (parent === at) {
34220
34383
  return path;
34221
34384
  }
34222
- missing.unshift(basename6(at));
34385
+ missing.unshift(basename7(at));
34223
34386
  at = parent;
34224
34387
  }
34225
34388
  }
@@ -34230,10 +34393,10 @@ function projectKey(projectPath) {
34230
34393
  return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
34231
34394
  }
34232
34395
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
34233
- return join23(directory, "runs", `note-${projectKey(projectPath)}.json`);
34396
+ return join24(directory, "runs", `note-${projectKey(projectPath)}.json`);
34234
34397
  }
34235
34398
  function latestRunPath(servedBy, directory = runtimeDirectory()) {
34236
- return join23(directory, "runs", `latest-${projectKey(servedBy)}.json`);
34399
+ return join24(directory, "runs", `latest-${projectKey(servedBy)}.json`);
34237
34400
  }
34238
34401
  function writeLatestRun(servedBy, latest) {
34239
34402
  try {
@@ -34259,17 +34422,20 @@ function readLatestRun(servedBy, directory = runtimeDirectory()) {
34259
34422
  var SHARED_NOTE = "run.json";
34260
34423
  function openTranscript(startedAt) {
34261
34424
  mkdirSync13(runsDirectory(), { recursive: true });
34262
- const path = join23(runsDirectory(), `run-${startedAt}.log`);
34425
+ const path = join24(runsDirectory(), `run-${startedAt}.log`);
34263
34426
  return { path, fd: openSync3(path, "a") };
34264
34427
  }
34265
34428
  function profilePath(startedAt) {
34266
- return join23(runsDirectory(), `run-${startedAt}.profile.json`);
34429
+ return join24(runsDirectory(), `run-${startedAt}.profile.json`);
34430
+ }
34431
+ function profileMarkPath(startedAt) {
34432
+ return join24(runsDirectory(), `run-${startedAt}.profile-mark.json`);
34267
34433
  }
34268
34434
  function noteIsAbout(note, playing) {
34269
34435
  return note.play !== undefined && playing !== null && note.play.play === playing.play && note.play.editorPid === playing.editorPid;
34270
34436
  }
34271
34437
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
34272
- return join23(directory, "runs", `editor-${projectKey(projectPath)}.json`);
34438
+ return join24(directory, "runs", `editor-${projectKey(projectPath)}.json`);
34273
34439
  }
34274
34440
  function writeEditorRunNote(note) {
34275
34441
  mkdirSync13(runsDirectory(), { recursive: true });
@@ -34280,7 +34446,7 @@ function writeEditorRunNote(note) {
34280
34446
  }
34281
34447
  function readEditorRunNote(projectPath) {
34282
34448
  for (const directory of runtimeDirectories()) {
34283
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join23(directory, "runs", "editor-run.json"));
34449
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join24(directory, "runs", "editor-run.json"));
34284
34450
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
34285
34451
  return note;
34286
34452
  }
@@ -34314,7 +34480,7 @@ function readRunRecord(projectPath) {
34314
34480
  if (own !== null) {
34315
34481
  return own;
34316
34482
  }
34317
- const shared = recordAt(join23(directory, "runs", SHARED_NOTE));
34483
+ const shared = recordAt(join24(directory, "runs", SHARED_NOTE));
34318
34484
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
34319
34485
  return shared;
34320
34486
  }
@@ -34326,13 +34492,13 @@ function everyRunRecord() {
34326
34492
  for (const directory of runtimeDirectories()) {
34327
34493
  let entries;
34328
34494
  try {
34329
- entries = readdirSync9(join23(directory, "runs"));
34495
+ entries = readdirSync10(join24(directory, "runs"));
34330
34496
  } catch {
34331
34497
  continue;
34332
34498
  }
34333
34499
  for (const entry of entries) {
34334
34500
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
34335
- const record = recordAt(join23(directory, "runs", entry));
34501
+ const record = recordAt(join24(directory, "runs", entry));
34336
34502
  if (record !== null) {
34337
34503
  found.push(record);
34338
34504
  }
@@ -34502,7 +34668,7 @@ function whyNotTheRun(record, running, needed) {
34502
34668
  if (after > SAME_RUN_WINDOW_MS) {
34503
34669
  return `that process started ${Math.round(after / 1000)} seconds after the run had`;
34504
34670
  }
34505
- const engine = record.command === undefined ? null : basename6(record.command);
34671
+ const engine = record.command === undefined ? null : basename7(record.command);
34506
34672
  const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
34507
34673
  const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
34508
34674
  if (wanted !== null && !said.includes(wanted)) {
@@ -34541,7 +34707,7 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
34541
34707
  }
34542
34708
  function clearRunRecord(projectPath) {
34543
34709
  for (const directory of runtimeDirectories()) {
34544
- const shared = join23(directory, "runs", SHARED_NOTE);
34710
+ const shared = join24(directory, "runs", SHARED_NOTE);
34545
34711
  const sharedRecord = recordAt(shared);
34546
34712
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
34547
34713
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
@@ -34553,7 +34719,7 @@ function clearRunRecord(projectPath) {
34553
34719
  }
34554
34720
  function ageMs(path, now) {
34555
34721
  try {
34556
- return now - statSync10(path).mtimeMs;
34722
+ return now - statSync11(path).mtimeMs;
34557
34723
  } catch {
34558
34724
  return 0;
34559
34725
  }
@@ -34562,20 +34728,21 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
34562
34728
  const directory = runsDirectory();
34563
34729
  const kept = new Set(everyRunRecord().flatMap((record) => [
34564
34730
  record.transcript,
34731
+ profileMarkPath(record.startedAt),
34565
34732
  ...record.profile === undefined ? [] : [record.profile]
34566
34733
  ]));
34567
34734
  let entries;
34568
34735
  try {
34569
- entries = readdirSync9(directory);
34736
+ entries = readdirSync10(directory);
34570
34737
  } catch {
34571
34738
  return;
34572
34739
  }
34573
34740
  for (const entry of entries) {
34574
- const transcript = entry.startsWith("run-") && (entry.endsWith(".log") || entry.endsWith(".profile.json"));
34741
+ const transcript = entry.startsWith("run-") && (entry.endsWith(".log") || entry.endsWith(".profile.json") || entry.endsWith(".profile-mark.json"));
34575
34742
  if (!transcript && !entry.endsWith(".partial")) {
34576
34743
  continue;
34577
34744
  }
34578
- const path = join23(directory, entry);
34745
+ const path = join24(directory, entry);
34579
34746
  if (kept.has(path) || ageMs(path, now) < keepMs) {
34580
34747
  continue;
34581
34748
  }
@@ -34586,7 +34753,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
34586
34753
  }
34587
34754
 
34588
34755
  // src/script-origins.ts
34589
- import { join as join24 } from "node:path";
34756
+ import { join as join25 } from "node:path";
34590
34757
  var UNRESOLVED = /Could not resolve class "([A-Za-z_][A-Za-z0-9_]*)", because of a parser error/;
34591
34758
  function unresolvedClassIn(message) {
34592
34759
  return UNRESOLVED.exec(message)?.[1] ?? null;
@@ -34642,7 +34809,7 @@ function checkWithEngine(godotPath, projectPath) {
34642
34809
  const ran = await runEngine(godotPath, [
34643
34810
  "--headless",
34644
34811
  "--log-file",
34645
- join24(logDir, "engine.log"),
34812
+ join25(logDir, "engine.log"),
34646
34813
  "--path",
34647
34814
  projectPath,
34648
34815
  "--check-only",
@@ -34736,6 +34903,52 @@ function isCalibration(signature) {
34736
34903
  const parts = signatureParts(signature);
34737
34904
  return parts.script.endsWith(RUNTIME_SCRIPT) && (parts.function === PROFILER_PROBE || parts.function === PROFILER_CALIBRATOR);
34738
34905
  }
34906
+ function writeProfileMark(path, mark) {
34907
+ return writeWhole(path, JSON.stringify(mark));
34908
+ }
34909
+ function readProfileMark(path) {
34910
+ try {
34911
+ const parsed = JSON.parse(readFileSync22(path, "utf8"));
34912
+ return typeof parsed === "object" && typeof parsed.totals === "object" ? parsed : null;
34913
+ } catch {
34914
+ return null;
34915
+ }
34916
+ }
34917
+ function totalsSince(now, mark) {
34918
+ if (now.frames < mark.frames || now.startedAt !== mark.startedAt) {
34919
+ return null;
34920
+ }
34921
+ let shrunk = 0;
34922
+ const functions = {};
34923
+ for (const [signature, sum] of Object.entries(now.functions)) {
34924
+ const before = mark.functions[signature];
34925
+ if (isCalibration(signature) || before === undefined) {
34926
+ functions[signature] = sum;
34927
+ continue;
34928
+ }
34929
+ const added = {
34930
+ calls: sum.calls - before.calls,
34931
+ selfSeconds: sum.selfSeconds - before.selfSeconds,
34932
+ totalSeconds: sum.totalSeconds - before.totalSeconds
34933
+ };
34934
+ if (added.calls < 0 || added.selfSeconds < 0 || added.totalSeconds < 0) {
34935
+ shrunk += 1;
34936
+ } else if (added.calls > 0) {
34937
+ functions[signature] = added;
34938
+ }
34939
+ }
34940
+ shrunk += Object.keys(mark.functions).filter((signature) => now.functions[signature] === undefined && (mark.functions[signature]?.calls ?? 0) > 0).length;
34941
+ return {
34942
+ totals: {
34943
+ ...now,
34944
+ frames: now.frames - mark.frames,
34945
+ unreadable: Math.max(0, now.unreadable - mark.unreadable),
34946
+ ...now.cappedFrames === undefined ? {} : { cappedFrames: Math.max(0, now.cappedFrames - (mark.cappedFrames ?? 0)) },
34947
+ functions
34948
+ },
34949
+ shrunk
34950
+ };
34951
+ }
34739
34952
  function perCallCostOf(totals) {
34740
34953
  for (const [signature, sum] of Object.entries(totals.functions)) {
34741
34954
  const parts = signatureParts(signature);
@@ -34899,6 +35112,8 @@ var SCENE_PATH = {
34899
35112
  type: "string",
34900
35113
  description: 'Scene file inside the project, such as "scenes/main.tscn" or "res://scenes/main.tscn".'
34901
35114
  };
35115
+ var 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.";
35116
+ var 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.';
34902
35117
  var SCRIPT_PATH = {
34903
35118
  type: "string",
34904
35119
  description: 'Script file inside the project, such as "scripts/player.gd".'
@@ -34911,7 +35126,7 @@ var NODE_PATH = {
34911
35126
  };
34912
35127
  var PROPERTIES = {
34913
35128
  type: "object",
34914
- description: 'Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. A key may also be a colon path into one of the components of a vector, a colour, a rectangle or a transform, as GDScript names them, so one axis is written alone: "position:x", "modulate:a", "region_rect:size:y"; a component the value does not have is refused, naming the ones it has. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds.',
35129
+ description: `Properties to set, keyed by Godot property name. Vectors, colours and the like may be written as {"x": 1, "y": 2} or tagged {"_type": "Vector2", "x": 1, "y": 2}, which is the form a read answers with, so a value read off one node can be written straight to another. A key may also be a colon path into one of the components of a vector, a colour, a rectangle or a transform, as GDScript names them, so one axis is written alone: "position:x", "modulate:a", "region_rect:size:y"; a component the value does not have is refused, naming the ones it has. A property holding a node takes the path to it from the node being set, such as "../Player". Each value is read back once set, and one the engine changed or refused is refused with what it holds. ${WHOLE_NUMBERS}`,
34915
35130
  additionalProperties: true
34916
35131
  };
34917
35132
  var XY = {
@@ -34955,7 +35170,10 @@ var SCRIPT_MODIFICATIONS = {
34955
35170
  name: { type: "string" },
34956
35171
  params: { type: "string", description: 'Functions and signals: the parameter list, "delta: float".' },
34957
35172
  returnType: { type: "string", description: "Functions: the return type." },
34958
- body: { type: "string", description: "Functions: the body." },
35173
+ body: {
35174
+ type: "string",
35175
+ description: "Functions: the body, its lines separated by \\n. Indent shared by every line is taken off and one tab put on, so it can be written indented or not."
35176
+ },
34959
35177
  varType: {
34960
35178
  type: "string",
34961
35179
  description: "Variables and constants: the type annotation; without one, the type the value evaluates to."
@@ -35058,7 +35276,7 @@ var TOOL_SPECS = [
35058
35276
  {
35059
35277
  name: "project_settings",
35060
35278
  title: "Project settings",
35061
- description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events and deadzone, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change touches only its own lines. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
35279
+ description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses. A write with an editor open on the project is handed to the editor, which takes the settings the write changed or names from the file (named under editorAdopted), including a setting the file already held, since the editor can hold another value after the file was put back, or reloads the bus layout, so it answers the new values and does not save the old ones back over them. editorNote is an editor that could not take the write, one on an addon from before this, which still holds the old values until editor_launch restart. An enabled plugin's code still loads on the editor's next start. set answers old_value and new_value, with was_new true when project.godot did not set the setting before, and was_default true when what it held was the engine's own default, which old_value then is. add_input_action on an action that exists, one of the engine's ui_ actions included, replaces its events and deadzone, and answers was_new false with what it held under old_events and old_deadzone. A set of the value the setting already holds, in the same type, writes nothing and answers unchanged true and saved false. Every write keeps the sections and keys of project.godot in the order the file had them, so a change moves no other line; the save is the engine's, and like the editor's it writes Godot's header and config/features into a file that lacks them. get answers exists, true for any setting the engine knows, and at_default, true when the value is the engine's own default and so has no line in project.godot. Each setting under prefix carries at_default too, so a family read separates what the project sets from the engine's defaults; it is absent for a setting the engine registers no default for, such as an input action the project added, which only the project can have set.",
35062
35280
  parameters: {
35063
35281
  projectPath: PROJECT_PATH2,
35064
35282
  setting: {
@@ -35236,24 +35454,32 @@ var TOOL_SPECS = [
35236
35454
  description: "The import pipeline: what needs importing, how a resource is imported, reimports, UIDs, and the global class list the editor and the engine read.",
35237
35455
  parameters: {
35238
35456
  projectPath: PROJECT_PATH2,
35239
- resourcePath: RESOURCE_PATH,
35457
+ resourcePath: { ...RESOURCE_PATH, ops: ["status", "options", "set_options", "uid", "reimport"] },
35240
35458
  includeUpToDate: {
35241
35459
  type: "boolean",
35460
+ ops: ["status"],
35242
35461
  description: "status: list resources that are current as well. Default false."
35243
35462
  },
35244
35463
  options: {
35245
35464
  type: "object",
35465
+ ops: ["set_options"],
35246
35466
  description: `set_options: import options keyed as the .import file spells them, {"compress/mode": 1}. A dictionary option whose keys are numbers, a font's opentype_features or any option already holding number keys, takes them written as strings of digits, and opentype_features also takes an OpenType tag by its four-letter name: {"opentype_features": {"liga": 0}} turns ligatures off. Both are written as the numbers the engine reads.`,
35247
35467
  additionalProperties: true
35248
35468
  },
35249
35469
  reimport: {
35250
35470
  type: "boolean",
35471
+ ops: ["set_options"],
35251
35472
  description: "set_options: reimport afterwards, answered under reimport as project_import reimport answers. Default true."
35252
35473
  },
35253
- force: { type: "boolean", description: "reimport: reimport even what is current. Default false." },
35474
+ force: {
35475
+ type: "boolean",
35476
+ ops: ["reimport"],
35477
+ description: "reimport: reimport even what is current. Default false."
35478
+ },
35254
35479
  timeoutMs: {
35255
35480
  type: "integer",
35256
35481
  minimum: 1,
35482
+ ops: ["reimport", "set_options"],
35257
35483
  description: "reimport, set_options: how long to wait for the editor to finish reimporting. Default 300000."
35258
35484
  }
35259
35485
  },
@@ -35331,7 +35557,7 @@ var TOOL_SPECS = [
35331
35557
  name: "project_test",
35332
35558
  title: "Run tests",
35333
35559
  effect: "additive",
35334
- 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, every report 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. failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
35560
+ description: "Runs the project's gdUnit4 tests headless and answers with every case that did not pass: where it is, and what the assertion said. Each entry under failed carries its first report as message and detail, and, when it has more than one, its reports under reports in the order the case met them: gdUnit4 goes on past a failed assertion, and a case can fail several and then stop on a runtime error, which makes its status error. Identical reports fold into one with times, at most twenty different ones are kept (reportsLeftOut counts the rest), and past about a megabyte the largest cases keep only their first message (failedWithoutDetail). failures counts the cases that failed an assertion and errors the cases that hit an error, so such a case counts in both. Suites where everything passed are counted rather than listed, and an engine message keeps the frames above gdUnit4 rather than the twenty inside it, so a clean tier answers in a few lines; identical engine messages come back once under engineEntries, with times saying how often they were printed, and engineErrors counts every one. The class list is rebuilt first, so a suite written a moment ago is found, and the rebuild's answer is under classes, with the classes each addon declares counted under addedInAddons and changedInAddons rather than named: a class it puts back that the cache had after the last rebuild is named under classes.lostSinceLastRebuild, which is the editor writing its shorter list over the file on every save until restarted. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. On macOS the engine reads user:// off HOME and the run writes where the player keeps theirs, which the answer says under savesNote rather than leaving to be found in a save list. A run that found nothing to run is never called a pass: gdUnit4 exits cleanly for one, so the answer says so and names the path it looked in. A script that does not load stops gdUnit4 before any suite runs: its errors come back in full under scriptErrors, and the scripts that failed only because one they depend on did are listed by path under failedThroughDependency. When they failed on a class whose own script has a parser error, which gdUnit4 never names, that script is checked by the engine and its error comes first under scriptErrors, with the class it declares under declares, and the errors a script has only because that class did not resolve, which the engine reads as Variant, are kept apart under followFromUnresolved. A client that sends a progress token is told as each suite finishes, and at least every twenty seconds through a long one, so an idle limit does not end a long tier. A run that ends without a report, at its timeout or otherwise, names the suites it finished under suitesFinished, the failing ones with their failed cases under suitesFailing, and the one it was in under suiteRunning. When a client cuts a run off, that is kept in the project and the next project_test there carries it under previousRunCut. A run where nothing failed but nodes were left in the tree comes back under warnings, with the count per suite: gdUnit4 decides its verdict on those and keeps them out of its report, so they are read off what it printed. A run whose every case passed is still not called passed when it printed a script error that no case failed on: gdUnit4 fails a case on the script errors its logger sees, and it cannot see a stack overflow, so the cases pass on whatever the faulted calls returned. Those errors come back under uncaughtScriptErrors, and under engineEntries by index with detailUnder rather than with their backtrace a second time; a backtrace that repeats, as a recursion's does, has each repeated frame or cycle of frames written once with the frames it covers and how many times. One raised inside assert_error(...).is_runtime_error is the test's own and does not count, and none count when the project turns off gdUnit4's report/godot/script_error setting. Needs gdUnit4 under addons/gdUnit4.",
35335
35561
  parameters: {
35336
35562
  projectPath: PROJECT_PATH2,
35337
35563
  path: {
@@ -35368,14 +35594,14 @@ var TOOL_SPECS = [
35368
35594
  {
35369
35595
  name: "scene_create",
35370
35596
  title: "Create or save a scene",
35371
- description: "Creates a scene file where none is, saves one, or saves a copy under a new path. A save of a scene open in the editor saves the editor's copy, unsaved changes included. Needs the editor connected.",
35597
+ description: `Creates a scene file where none is, saves one, or saves a copy under a new path. A save of a scene open in the editor saves the editor's copy, unsaved changes included. ${WITHOUT_AN_EDITOR}`,
35372
35598
  parameters: {
35373
35599
  projectPath: PROJECT_PATH2,
35374
35600
  scenePath: SCENE_PATH,
35375
35601
  rootNodeType: {
35376
35602
  type: "string",
35377
35603
  ops: ["create"],
35378
- description: "create: the root node class. Default Node."
35604
+ description: "create: the root node class, an engine class or a class_name a project script declares, which the root is made as with that script attached. Default Node."
35379
35605
  },
35380
35606
  rootNodeName: {
35381
35607
  type: "string",
@@ -35413,29 +35639,32 @@ var TOOL_SPECS = [
35413
35639
  {
35414
35640
  name: "scene_node",
35415
35641
  title: "Scene nodes",
35416
- description: "One node in a scene file: add, read, set, duplicate, reparent or delete it, or paint TileMap cells. Any ClassDB node type can be added, so a NavigationRegion2D, an AnimationTree or a Camera3D is an add with that nodeType and its properties, and another scene is instanced by an add with instancePath. set with name renames a node. A property holding a Resource takes the res:// path of one, so a texture, a material or a theme is a set like any other. A write is refused while the editor holds unsaved changes to the scene, and for a node that belongs to a scene this one instances or inherits. Needs the editor connected.",
35642
+ description: `One node in a scene file: add, read, set, duplicate, reparent or delete it, or paint TileMap cells. Any ClassDB node type or script class can be added, so a NavigationRegion2D, an AnimationTree or a Camera3D is an add with that nodeType and its properties, and another scene is instanced by an add with instancePath. A set with properties {"name": "Boss"} renames a node. A property holding a Resource takes the res:// path of one, so a texture, a material or a theme is a set like any other. A write is refused while the editor holds unsaved changes to the scene, and for a node that belongs to a scene this one instances or inherits. ${WITHOUT_AN_EDITOR}`,
35417
35643
  parameters: {
35418
35644
  projectPath: PROJECT_PATH2,
35419
35645
  scenePath: SCENE_PATH,
35420
- nodePath: NODE_PATH,
35646
+ nodePath: { ...NODE_PATH, ops: ["get", "set", "duplicate", "reparent", "delete", "set_tilemap_cells"] },
35421
35647
  parentNodePath: {
35422
35648
  type: "string",
35649
+ ops: ["add", "duplicate"],
35423
35650
  description: "add: where the node goes. Default the root. duplicate: where the copy goes."
35424
35651
  },
35425
35652
  nodeType: {
35426
35653
  type: "string",
35427
- description: 'add: the node class, such as "CharacterBody2D". Give this or instancePath.'
35654
+ ops: ["add"],
35655
+ description: 'add: the node class, such as "CharacterBody2D", or a class_name a project script declares, which the node is made as with that script attached. Give this or instancePath.'
35428
35656
  },
35429
35657
  instancePath: {
35430
35658
  type: "string",
35431
35659
  ops: ["add"],
35432
35660
  description: `add: a scene to instance as the new node instead of a class, such as "ui/panel.tscn". It is saved as an instance of that file, the way the editor's Instantiate Child Scene saves one, so changes to that scene reach it; properties set its root. A scene that is the target or instances it is refused.`
35433
35661
  },
35434
- nodeName: { type: "string", description: "add: the new node's name." },
35435
- properties: PROPERTIES,
35436
- newName: { type: "string", description: "duplicate: the copy's name." },
35662
+ nodeName: { type: "string", ops: ["add"], description: "add: the new node's name." },
35663
+ properties: { ...PROPERTIES, ops: ["add", "set"] },
35664
+ newName: { type: "string", ops: ["duplicate"], description: "duplicate: the copy's name." },
35437
35665
  newParentPath: {
35438
35666
  type: "string",
35667
+ ops: ["reparent"],
35439
35668
  description: "reparent: the new parent, which may be the one it has, to move it among its siblings."
35440
35669
  },
35441
35670
  index: {
@@ -35446,13 +35675,15 @@ var TOOL_SPECS = [
35446
35675
  },
35447
35676
  includeDefaults: {
35448
35677
  type: "boolean",
35678
+ ops: ["get"],
35449
35679
  description: "get: include properties still at their default. Default false."
35450
35680
  },
35451
35681
  layer: {
35452
35682
  type: "number",
35683
+ ops: ["set_tilemap_cells"],
35453
35684
  description: "set_tilemap_cells: the layer of a TileMap. Default 0. A TileMapLayer is one layer."
35454
35685
  },
35455
- cells: TILEMAP_CELLS
35686
+ cells: { ...TILEMAP_CELLS, ops: ["set_tilemap_cells"] }
35456
35687
  },
35457
35688
  requires: ["projectPath", "scenePath"],
35458
35689
  operations: {
@@ -35492,16 +35723,32 @@ var TOOL_SPECS = [
35492
35723
  {
35493
35724
  name: "scene_signal",
35494
35725
  title: "Scene signals",
35495
- 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. Needs the editor connected.",
35726
+ 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}`,
35496
35727
  parameters: {
35497
35728
  projectPath: PROJECT_PATH2,
35498
35729
  scenePath: SCENE_PATH,
35499
- sourceNodePath: { type: "string", description: "The node that emits." },
35500
- signalName: { type: "string", description: 'The signal it emits, such as "pressed".' },
35501
- targetNodePath: { type: "string", description: "The node whose method is called." },
35502
- methodName: { type: "string", description: "The method on that node, called when it emits." },
35503
- flags: { type: "number", description: "connect: Object.ConnectFlags, such as 1 for deferred." },
35504
- nodePath: { type: "string", description: "list: only connections involving this node." }
35730
+ sourceNodePath: { type: "string", ops: ["connect", "disconnect"], description: "The node that emits." },
35731
+ signalName: {
35732
+ type: "string",
35733
+ ops: ["connect", "disconnect"],
35734
+ description: 'The signal it emits, such as "pressed".'
35735
+ },
35736
+ targetNodePath: {
35737
+ type: "string",
35738
+ ops: ["connect", "disconnect"],
35739
+ description: "The node whose method is called."
35740
+ },
35741
+ methodName: {
35742
+ type: "string",
35743
+ ops: ["connect", "disconnect"],
35744
+ description: "The method on that node, called when it emits."
35745
+ },
35746
+ flags: {
35747
+ type: "number",
35748
+ ops: ["connect"],
35749
+ description: "connect: Object.ConnectFlags, such as 1 for deferred."
35750
+ },
35751
+ nodePath: { type: "string", ops: ["list"], description: "list: only connections involving this node." }
35505
35752
  },
35506
35753
  requires: ["projectPath", "scenePath"],
35507
35754
  operations: {
@@ -35626,18 +35873,28 @@ var TOOL_SPECS = [
35626
35873
  parameters: {
35627
35874
  projectPath: PROJECT_PATH2,
35628
35875
  scriptPath: SCRIPT_PATH,
35629
- className: { type: "string", description: "create: a class_name for the script. Not with content." },
35630
- extends: { type: "string", description: "create: the base class. Default Node. Not with content." },
35876
+ className: {
35877
+ type: "string",
35878
+ ops: ["create"],
35879
+ description: "create: a class_name for the script. Not with content."
35880
+ },
35881
+ extends: {
35882
+ type: "string",
35883
+ ops: ["create"],
35884
+ description: "create: the base class. Default Node. Not with content."
35885
+ },
35631
35886
  content: {
35632
35887
  type: "string",
35888
+ ops: ["create"],
35633
35889
  description: "create: the whole file, written as given, instead of a template, className and extends."
35634
35890
  },
35635
35891
  template: {
35636
35892
  type: "string",
35637
35893
  enum: ["singleton", "state_machine", "component", "resource"],
35894
+ ops: ["create"],
35638
35895
  description: "create: a starting shape."
35639
35896
  },
35640
- modifications: SCRIPT_MODIFICATIONS,
35897
+ modifications: { ...SCRIPT_MODIFICATIONS, ops: ["modify"] },
35641
35898
  symbol: {
35642
35899
  type: "string",
35643
35900
  ops: ["rename"],
@@ -35675,16 +35932,19 @@ var TOOL_SPECS = [
35675
35932
  scriptPath: SCRIPT_PATH,
35676
35933
  includeInherited: {
35677
35934
  type: "boolean",
35935
+ ops: ["structure"],
35678
35936
  description: "structure: also list what the script inherits, walking extends through the project's other scripts. Each such member carries inherited_from, the file declaring it, and the answer gains inherits_from, the chain that was walked. A name the script overrides is listed at both its lines rather than once. A native base is not walked, since it declares nothing in a file: editor_classes info answers for those. Default false."
35679
35937
  },
35680
35938
  line: {
35681
35939
  type: "integer",
35682
35940
  minimum: 1,
35941
+ ops: ["completion", "hover"],
35683
35942
  description: "completion, hover: the line, counted from 1 as the editor shows it."
35684
35943
  },
35685
35944
  character: {
35686
35945
  type: "integer",
35687
35946
  minimum: 1,
35947
+ ops: ["completion", "hover"],
35688
35948
  description: "completion, hover: the column, counted from 1 as the editor shows it, with a tab counted as one. symbols gives each name at the line and column hover is asked at."
35689
35949
  }
35690
35950
  },
@@ -35692,7 +35952,7 @@ var TOOL_SPECS = [
35692
35952
  operations: {
35693
35953
  structure: {
35694
35954
  effect: "read",
35695
- summary: "functions, variables, signals, class_name and extends, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class",
35955
+ summary: "functions, variables, signals, class_name and extends, read from the file, and dependencies: the paths it loads or preloads as written, then the scripts declaring each project class_name its code uses, which classes_used names with the class; autoloads_used names each autoload the code uses by its global name, with the autoload's script, which the project loads rather than this script, so it is not under dependencies",
35696
35956
  requires: []
35697
35957
  },
35698
35958
  symbols: {
@@ -35727,7 +35987,7 @@ var TOOL_SPECS = [
35727
35987
  {
35728
35988
  name: "resource_edit",
35729
35989
  title: "Edit resources",
35730
- description: 'Resource files: read one, create any ClassDB resource as .tres, change one, write a shader, build a TileSet, or set a Theme colour or font size. A material is a create with resourceType StandardMaterial3D, ShaderMaterial or CanvasItemMaterial. read answers with the properties the file sets, in the form modify takes, and a Theme lists its items there as "Button/colors/font_color". move takes any file of the project, a script or an image as much as a resource, or a directory, and works from the files, so it needs no editor; one serving the project is brought up to it as after a move in its own FileSystem dock, its open scenes and cached resources named for the new paths and the project settings the move rewrote taken from the file. read needs no editor either: with none serving the project, a headless engine reads the file. The ops that write a resource need the editor connected.',
35990
+ description: 'Resource files: read one, create any ClassDB resource as .tres, change one, write a shader, build a TileSet, or set a Theme colour or font size. A material is a create with resourceType StandardMaterial3D, ShaderMaterial or CanvasItemMaterial. read answers with the properties the file sets, in the form modify takes, and a Theme lists its items there as "Button/colors/font_color". move takes any file of the project, a script or an image as much as a resource, or a directory, and works from the files, so it needs no editor; one serving the project is brought up to it as after a move in its own FileSystem dock, its open scenes and cached resources named for the new paths and the project settings the move rewrote taken from the file. read needs no editor either: with none serving the project, a headless engine reads the file. The ops that write a resource go through the editor when one serves the project; with none, a headless engine runs the tool the editor would and the answer says so under writtenBy. That engine runs the script of the resource, 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 the save, and a resource where that cannot be done is refused and the file left as it was.',
35731
35991
  parameters: {
35732
35992
  projectPath: PROJECT_PATH2,
35733
35993
  resourcePath: {
@@ -35737,7 +35997,7 @@ var TOOL_SPECS = [
35737
35997
  resourceType: {
35738
35998
  type: "string",
35739
35999
  ops: ["create"],
35740
- description: 'create: the resource class, such as "PhysicsMaterial" or "StandardMaterial3D".'
36000
+ description: 'create: the resource class, such as "PhysicsMaterial" or "StandardMaterial3D", or a class_name a project script declares, which the resource is made as with that script attached.'
35741
36001
  },
35742
36002
  properties: { ...PROPERTIES, ops: ["create", "modify"] },
35743
36003
  script: {
@@ -35931,6 +36191,17 @@ var TOOL_SPECS = [
35931
36191
  ops: ["profile"],
35932
36192
  description: "profile: also answer, under callers, where the project's scripts name this function in code: its name, or Class.name for the functions of one class. Each place has its script, line, the named function it is written in (within), whether the function is called there or passed on as a Callable (called), and the profile's calls, self and total milliseconds for the function it is in, those taking the most time first, forty at most. Found by name in the source, since the engine's profiler records nothing about who made a call, so a method of the same name on another class is listed too, except, when Class.name was asked, one called through another class of the project by name or through a parameter, local or class variable declared as one, and a call made through a string is not. A parameter, local or class variable of the same name is not the function and is not listed. A property's get or set block, which the profile names Class.@name_getter or Class.@name_setter, is asked about by that name or by the property's: the places are where the property is named, each saying instead of called whether it writes the property (access write), which runs the set block, or reads it (access read), which runs the get block, and the rows are both blocks'. Inside the property's own blocks its name is the value stored, which the engine reads and writes without running them, so those lines are not places."
35933
36193
  },
36194
+ mark: {
36195
+ type: "boolean",
36196
+ ops: ["profile"],
36197
+ description: `profile: answer as usual and set the run's mark at these totals, so a later profile with since: "mark" answers for one action, a press or a commit, apart from the boot and everything before it. The mark is kept beside the run's profile, so a server that replaces this one reads it too, and setting it again moves it. Given with since, the answer counts from the mark before it moves, which times one span after another. Default false.`
36198
+ },
36199
+ since: {
36200
+ type: "string",
36201
+ enum: ["mark"],
36202
+ ops: ["profile"],
36203
+ description: 'profile: "mark" answers only what was added after the mark that mark: true set in this run: per function the calls and self and total milliseconds since it, with the functions that added no call left out, frames and coveredMs since it, and callersOf read off the same difference. Refused when no mark was set in this run, or when the profile holds less than it did at the mark, which is a profiler started again since.'
36204
+ },
35934
36205
  limit: {
35935
36206
  type: "integer",
35936
36207
  minimum: 1,
@@ -35982,7 +36253,7 @@ var TOOL_SPECS = [
35982
36253
  requires: []
35983
36254
  }
35984
36255
  },
35985
- 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. 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. 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.",
36256
+ 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. 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. 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.",
35986
36257
  defaultOperation: "start"
35987
36258
  },
35988
36259
  {
@@ -36078,9 +36349,10 @@ var TOOL_SPECS = [
36078
36349
  description: "The engine's ClassDB: find classes, read one in full, or walk an inheritance tree.",
36079
36350
  parameters: {
36080
36351
  projectPath: PROJECT_PATH2,
36081
- filter: { type: "string", description: "query: a substring of the class name." },
36352
+ filter: { type: "string", ops: ["query"], description: "query: a substring of the class name." },
36082
36353
  category: {
36083
36354
  type: "string",
36355
+ ops: ["query"],
36084
36356
  enum: [
36085
36357
  "node",
36086
36358
  "node2d",
@@ -36097,9 +36369,21 @@ var TOOL_SPECS = [
36097
36369
  ],
36098
36370
  description: "query: limit to one family."
36099
36371
  },
36100
- instantiableOnly: { type: "boolean", description: "query: leave out abstract classes. Default false." },
36101
- className: { type: "string", description: "info, inheritance: the class." },
36102
- includeInherited: { type: "boolean", description: "info: include inherited members. Default false." },
36372
+ instantiableOnly: {
36373
+ type: "boolean",
36374
+ ops: ["query"],
36375
+ description: "query: leave out abstract classes. Default false."
36376
+ },
36377
+ className: {
36378
+ type: "string",
36379
+ ops: ["info", "inheritance"],
36380
+ description: "info, inheritance: the class."
36381
+ },
36382
+ includeInherited: {
36383
+ type: "boolean",
36384
+ ops: ["info"],
36385
+ description: "info: include inherited members. Default false."
36386
+ },
36103
36387
  member: {
36104
36388
  type: "string",
36105
36389
  ops: ["info"],
@@ -36253,7 +36537,7 @@ var TOOL_SPECS = [
36253
36537
  value: {
36254
36538
  ops: ["set"],
36255
36539
  blank: true,
36256
- description: `set: the value, fitted to the property's type. A number the property would have to cut is refused rather than truncated: a fraction into an int, a number other than 0 or 1 into a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent, a fraction into one holding an int included. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
36540
+ description: `set: the value, fitted to the property's type. ${WHOLE_NUMBERS} A number the property would have to cut is refused rather than truncated: a fraction into an int, a number other than 0 or 1 into a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}, which is the form a read answers with. "" writes an empty string. A property typed as or holding an object takes a path naming one the game holds, the way call's args do, and holds that instance, or null, which the text null also means, to empty it. One declared as Variant or untyped takes anything, as the engine does: a value of the kind it holds is fitted to it, an int staying an int, and any other is stored as sent, a fraction into one holding an int included. A typed list or map, such as Array[int] or Dictionary[String, int], takes a JSON list or object with each element fitted to its element type, and an element that cannot become one is refused by its index or key, as is an object of another script than the one the list holds, whether or not that script has a class_name.`
36257
36541
  },
36258
36542
  method: {
36259
36543
  type: "string",
@@ -36263,7 +36547,7 @@ var TOOL_SPECS = [
36263
36547
  args: {
36264
36548
  type: "array",
36265
36549
  ops: ["call"],
36266
- description: `call: the arguments, fitted to the method's parameter types. A number a parameter would have to cut is refused rather than truncated: a fraction for an int, a number other than 0 or 1 for a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
36550
+ description: `call: the arguments, fitted to the method's parameter types. An RID is passed as the {"_type": "RID", "id": ...} a read answers it with, such as a viewport's for RenderingServer's render timing. ${WHOLE_NUMBERS} A number a parameter would have to cut is refused rather than truncated: a fraction for an int, a number other than 0 or 1 for a bool. A vector, colour or the like may be written as its fields, {"x": 140, "y": 300}, or tagged, {"_type": "Vector2", "x": 140, "y": 300}. A parameter typed as an object takes a path naming one the game holds and is handed that instance: a colon path read from nodePath, "_game:run:wares:3", or one starting at a node, "/root/Main/Hud" or "/root/Main:_game:run". A path that reaches no object, or an object of another class than the parameter declares, is refused, and so are too few or too many arguments. The engine names a parameter typed as a script class without class_name by the class the script extends, so an object of that class with another script passes that check and the engine refuses the call: the answer says the method did not run, with the engine's reason. A parameter typed as a list or map, such as Array[int] or Array[Gear], is built from a JSON list or object element by element, objects named by their paths. The method itself takes its arguments here only: brackets on it holding arguments are refused.`
36267
36551
  },
36268
36552
  properties: {
36269
36553
  type: ["boolean", "array"],
@@ -36767,16 +37051,16 @@ function serverInstructions() {
36767
37051
  import { createHash as createHash5 } from "node:crypto";
36768
37052
  import {
36769
37053
  cpSync,
36770
- existsSync as existsSync19,
37054
+ existsSync as existsSync20,
36771
37055
  mkdirSync as mkdirSync14,
36772
- readdirSync as readdirSync10,
37056
+ readdirSync as readdirSync11,
36773
37057
  readFileSync as readFileSync23,
36774
37058
  rmdirSync,
36775
37059
  rmSync as rmSync12,
36776
- statSync as statSync11,
37060
+ statSync as statSync12,
36777
37061
  writeFileSync as writeFileSync15
36778
37062
  } from "node:fs";
36779
- import { dirname as dirname17, join as join25 } from "node:path";
37063
+ import { dirname as dirname18, join as join26 } from "node:path";
36780
37064
  import { fileURLToPath as fileURLToPath3 } from "node:url";
36781
37065
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
36782
37066
  var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
@@ -36795,13 +37079,13 @@ function runtimeAddonDigest(from = shippedAddonsDirectory()) {
36795
37079
  function addonsDigest(names, from) {
36796
37080
  const hash = createHash5("sha256");
36797
37081
  const walk = (directory, relative) => {
36798
- for (const name of readdirSync10(directory).sort()) {
37082
+ for (const name of readdirSync11(directory).sort()) {
36799
37083
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
36800
37084
  continue;
36801
37085
  }
36802
- const path = join25(directory, name);
37086
+ const path = join26(directory, name);
36803
37087
  const inside = `${relative}/${name}`;
36804
- if (statSync11(path).isDirectory()) {
37088
+ if (statSync12(path).isDirectory()) {
36805
37089
  walk(path, inside);
36806
37090
  } else {
36807
37091
  hash.update(`${inside}
@@ -36813,7 +37097,7 @@ function addonsDigest(names, from) {
36813
37097
  }
36814
37098
  };
36815
37099
  for (const name of names) {
36816
- walk(join25(from, name), name);
37100
+ walk(join26(from, name), name);
36817
37101
  }
36818
37102
  return hash.digest("hex");
36819
37103
  }
@@ -36846,19 +37130,19 @@ function installedRuntimeDigest(projectPath) {
36846
37130
  return digestMarker(projectPath, RUNTIME_ADDON);
36847
37131
  }
36848
37132
  function digestMarker(projectPath, addon) {
36849
- const marker = join25(projectPath, "addons", addon, DIGEST_MARKER);
36850
- if (!existsSync19(marker)) {
37133
+ const marker = join26(projectPath, "addons", addon, DIGEST_MARKER);
37134
+ if (!existsSync20(marker)) {
36851
37135
  return null;
36852
37136
  }
36853
37137
  const said = readFileSync23(marker, "utf8").trim();
36854
37138
  return said === "" ? null : said;
36855
37139
  }
36856
37140
  function shippedAddonsDirectory() {
36857
- return join25(dirname17(fileURLToPath3(import.meta.url)), "godot", "addons");
37141
+ return join26(dirname18(fileURLToPath3(import.meta.url)), "godot", "addons");
36858
37142
  }
36859
37143
  function installedAddonVersion(projectPath) {
36860
- const marker = join25(projectPath, "addons", ADDONS[0], VERSION_MARKER);
36861
- if (!existsSync19(marker)) {
37144
+ const marker = join26(projectPath, "addons", ADDONS[0], VERSION_MARKER);
37145
+ if (!existsSync20(marker)) {
36862
37146
  return null;
36863
37147
  }
36864
37148
  const said = readFileSync23(marker, "utf8").trim();
@@ -36872,7 +37156,7 @@ function loaderAmong(projectPath, named) {
36872
37156
  }
36873
37157
  let source = null;
36874
37158
  try {
36875
- source = readFileSync23(join25(projectPath, path), "utf8");
37159
+ source = readFileSync23(join26(projectPath, path), "utf8");
36876
37160
  } catch {}
36877
37161
  if (source?.includes(RUNTIME_AUTOLOAD.path)) {
36878
37162
  return { name: entry, path, exists: true };
@@ -36914,10 +37198,10 @@ function enabledPlugins(settings) {
36914
37198
  function inspectProject(projectPath) {
36915
37199
  const problems = [];
36916
37200
  const addons = ADDONS.map((name) => {
36917
- const directory = join25(projectPath, "addons", name);
36918
- const marker = join25(directory, VERSION_MARKER);
36919
- const installed = existsSync19(directory);
36920
- const version = existsSync19(marker) ? readFileSync23(marker, "utf8").trim() : null;
37201
+ const directory = join26(projectPath, "addons", name);
37202
+ const marker = join26(directory, VERSION_MARKER);
37203
+ const installed = existsSync20(directory);
37204
+ const version = existsSync20(marker) ? readFileSync23(marker, "utf8").trim() : null;
36921
37205
  const current = version === SERVER_VERSION;
36922
37206
  if (!installed) {
36923
37207
  problems.push(`addons/${name} is not installed; run gdharness setup`);
@@ -36926,7 +37210,7 @@ function inspectProject(projectPath) {
36926
37210
  }
36927
37211
  return { name, installed, version, current };
36928
37212
  });
36929
- const settings = parseProjectGodot(readFileSync23(join25(projectPath, "project.godot"), "utf8"));
37213
+ const settings = parseProjectGodot(readFileSync23(join26(projectPath, "project.godot"), "utf8"));
36930
37214
  const pluginsEnabled = enabledPlugins(settings);
36931
37215
  for (const name of EDITOR_PLUGINS) {
36932
37216
  if (!pluginsEnabled.includes(name)) {
@@ -36942,7 +37226,7 @@ function inspectProject(projectPath) {
36942
37226
  const tracked = trackedByGit(projectPath, [...named.values()]);
36943
37227
  const addonMissing = (path) => addons.some((addon) => !addon.installed && path.startsWith(`addons/${addon.name}/`));
36944
37228
  for (const [name, path] of named) {
36945
- if (!existsSync19(join25(projectPath, path))) {
37229
+ if (!existsSync20(join26(projectPath, path))) {
36946
37230
  if (!addonMissing(path)) {
36947
37231
  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`);
36948
37232
  }
@@ -36992,7 +37276,9 @@ function argumentShape(schema) {
36992
37276
  // src/server.ts
36993
37277
  var UPDATE_NOTICE_EVERY = 500;
36994
37278
  var FEEDBACK_NOTICE_EVERY = 250;
36995
- var __dirname2 = dirname18(fileURLToPath4(import.meta.url));
37279
+ var FAILED_BUDGET = 1e6;
37280
+ var ANSWER_BUDGET = 4000000;
37281
+ var __dirname2 = dirname19(fileURLToPath4(import.meta.url));
36996
37282
  var PARENT_ASKED_AFTER_MS = 5000;
36997
37283
  function editorRestartTimeoutMs() {
36998
37284
  return waitFromEnv("GDHARNESS_EDITOR_RESTART_TIMEOUT_MS", 90000);
@@ -37024,22 +37310,22 @@ function writtenFileRefusal(path, rule) {
37024
37310
  if (/^(res|user):\/\//.test(path)) {
37025
37311
  return `${named} ${path} is a Godot path; ${what} is saved outside the project, at an absolute path on this machine.`;
37026
37312
  }
37027
- if (!isAbsolute4(path)) {
37313
+ if (!isAbsolute5(path)) {
37028
37314
  return `${named} ${path} is not absolute; name the whole path, so where it is written does not depend on this server's directory.`;
37029
37315
  }
37030
37316
  if (!rule.extensions.includes(extname4(path).toLowerCase())) {
37031
37317
  return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
37032
37318
  }
37033
- const directory = realPathOr(dirname18(path));
37034
- if (!existsSync20(directory)) {
37035
- return `${named}'s directory ${dirname18(path)} does not exist; ${what} creates no directories.`;
37319
+ const directory = realPathOr(dirname19(path));
37320
+ if (!existsSync21(directory)) {
37321
+ return `${named}'s directory ${dirname19(path)} does not exist; ${what} creates no directories.`;
37036
37322
  }
37037
- const where = join26(directory, basename7(path));
37323
+ const where = join27(directory, basename8(path));
37038
37324
  const inside = rule.projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
37039
37325
  if (inside !== undefined) {
37040
37326
  return `${named} ${path} is inside the project at ${inside}; ${what} is saved outside it, so it never becomes part of the game.`;
37041
37327
  }
37042
- const taken = rule.writes(where).find((file) => existsSync20(file));
37328
+ const taken = rule.writes(where).find((file) => existsSync21(file));
37043
37329
  if (taken !== undefined) {
37044
37330
  return taken === where ? `${named} ${path} already exists; ${what} is not written over a file. Name a new one.` : `${named} ${path} would write ${taken}, which already exists; ${what} is not written over a file. Name a new one.`;
37045
37331
  }
@@ -37150,7 +37436,7 @@ function exited(run) {
37150
37436
  return run.exitCode !== null || run.exitSignal !== null;
37151
37437
  }
37152
37438
  function howItExited(run) {
37153
- return run.exitSignal === null ? `exit code ${run.exitCode ?? "unknown"}` : `ended by ${run.exitSignal}, with no exit code`;
37439
+ return run.exitSignal === null ? exitCodeText(run.exitCode) : `ended by ${run.exitSignal}, with no exit code`;
37154
37440
  }
37155
37441
  function errorsUnread(run) {
37156
37442
  return run.throughEditor && typeof run.errorReport !== "string";
@@ -37263,7 +37549,7 @@ function endedWithoutACode(run) {
37263
37549
  function ourRunEnded(run, now) {
37264
37550
  const which = `${run.throughEditor ? "played in the editor" : "started"}${run.pid === null ? "" : ` (pid ${run.pid})`}`;
37265
37551
  const when = run.endedAt === undefined ? "" : ` ${Math.max(0, Math.round((now - run.endedAt) / 1000))}s ago`;
37266
- const how = run.endedThrough === "close" ? "this server asked it to close" : run.endedThrough === "keeper" ? "this server ended it through the keeper holding it" : run.endedThrough === "signal" ? "this server signalled it" : run.endedThrough === "editor" ? "this server asked the editor to stop it" : run.exitCode !== null ? `it exited by itself with code ${run.exitCode}` : run.exitSignal === null ? "it is no longer running" : `it was ended by ${run.exitSignal}`;
37552
+ const how = run.endedThrough === "close" ? "this server asked it to close" : run.endedThrough === "keeper" ? "this server ended it through the keeper holding it" : run.endedThrough === "signal" ? "this server signalled it" : run.endedThrough === "editor" ? "this server asked the editor to stop it" : run.exitCode !== null ? `it exited by itself with ${exitCodeText(run.exitCode)}` : run.exitSignal === null ? "it is no longer running" : `it was ended by ${run.exitSignal}`;
37267
37553
  return `No game with the runtime addon is running: the game this server ${which} ended${when}, and ${how}. editor_output has what it printed, and editor_run start runs it again.`;
37268
37554
  }
37269
37555
  function stopVerdict(stop) {
@@ -37601,7 +37887,7 @@ function notRunNote(notRun, failFast) {
37601
37887
  function exportPresetRefusal(projectPath, preset) {
37602
37888
  let text;
37603
37889
  try {
37604
- text = readFileSync24(join26(projectPath, "export_presets.cfg"), "utf8");
37890
+ text = readFileSync24(join27(projectPath, "export_presets.cfg"), "utf8");
37605
37891
  } catch {
37606
37892
  return `This project has no export_presets.cfg, so it has no preset '${preset}' to export with. project_export add_preset writes one, as the editor's Project > Export does.`;
37607
37893
  }
@@ -37623,7 +37909,7 @@ function exportPresetRefusal(projectPath, preset) {
37623
37909
  }
37624
37910
  return names.length === 0 ? `export_presets.cfg names no preset, so there is no '${preset}' to export with. project_export add_preset adds one.` : `This project has no export preset named '${preset}'. Its presets are ${names.map((name) => `'${name}'`).join(", ")}.`;
37625
37911
  }
37626
- function exportAnswer(ending, asked, before, after, beside = []) {
37912
+ function exportAnswer(ending, asked, before, after, beside = [], docCacheNote = null) {
37627
37913
  const { log } = ending;
37628
37914
  const written = after !== null && after !== before;
37629
37915
  const notCopied = librariesNotCopied(log.all);
@@ -37640,6 +37926,7 @@ function exportAnswer(ending, asked, before, after, beside = []) {
37640
37926
  librariesNotLoaded: notCopied,
37641
37927
  extensionNote: `The export could not load ${notCopied.join(" and ")}: on Windows an engine running as the editor, which an export is, loads a GDExtension from a ~ copy beside the library, and another engine on this project held that copy: it could not be moved aside before the export, or another engine made a new one in between. Export again once that engine has loaded or exited.`
37642
37928
  },
37929
+ ...docCacheNote === null ? {} : { docCacheNote },
37643
37930
  ...foldedEntriesAnswer(log)
37644
37931
  };
37645
37932
  if (verdict.exported) {
@@ -37664,9 +37951,9 @@ function writtenBeside(before, after, output, within) {
37664
37951
  function filesIn(folder) {
37665
37952
  const found = new Map;
37666
37953
  try {
37667
- for (const entry of readdirSync11(folder, { withFileTypes: true })) {
37954
+ for (const entry of readdirSync12(folder, { withFileTypes: true })) {
37668
37955
  if (entry.isFile()) {
37669
- found.set(entry.name, statSync12(join26(folder, entry.name)).mtimeMs);
37956
+ found.set(entry.name, statSync13(join27(folder, entry.name)).mtimeMs);
37670
37957
  }
37671
37958
  }
37672
37959
  } catch {}
@@ -37700,7 +37987,7 @@ function unsavedInEditorNote(scripts) {
37700
37987
  return `${named} ${one ? "is" : "are"} open in the editor's script ${one ? "tab" : "tabs"} with unsaved edits, and saving ${one ? "that tab" : "them"} there writes ${one ? "its" : "their"} text over what was written here: close ${one ? "it" : "them"} without saving, or save first and edit again.`;
37701
37988
  }
37702
37989
  function editorAddonMissing(projectPath) {
37703
- if (!existsSync20(join26(projectPath, "project.godot"))) {
37990
+ if (!existsSync21(join27(projectPath, "project.godot"))) {
37704
37991
  return null;
37705
37992
  }
37706
37993
  const report = inspectProject(projectPath);
@@ -37988,7 +38275,7 @@ function profileFrameFunctions(projectPath) {
37988
38275
  let projectGodot = null;
37989
38276
  if (projectPath !== null) {
37990
38277
  try {
37991
- projectGodot = readFileSync24(join26(projectPath, "project.godot"), "utf8");
38278
+ projectGodot = readFileSync24(join27(projectPath, "project.godot"), "utf8");
37992
38279
  } catch {}
37993
38280
  }
37994
38281
  return frameFunctionsFor(queuedMessagesOf(projectGodot));
@@ -38166,7 +38453,7 @@ function foldedEntriesAnswer(log, floor = "warning") {
38166
38453
  var ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd";
38167
38454
  function gdUnitFailsOnScriptErrors(projectPath) {
38168
38455
  try {
38169
- return settingKeys(readFileSync24(join26(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
38456
+ return settingKeys(readFileSync24(join27(projectPath, "project.godot"), "utf8")).get("gdunit4/report/godot/script_error") !== false;
38170
38457
  } catch {
38171
38458
  return true;
38172
38459
  }
@@ -38181,13 +38468,13 @@ function uncaughtNote(uncaught) {
38181
38468
  }
38182
38469
  function elsewhereIn(projectPath, asked) {
38183
38470
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
38184
- if (wanted === "" || existsSync20(join26(projectPath, wanted))) {
38471
+ if (wanted === "" || existsSync21(join27(projectPath, wanted))) {
38185
38472
  return "";
38186
38473
  }
38187
38474
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
38188
38475
  let near = [];
38189
38476
  try {
38190
- near = readdirSync11(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
38477
+ near = readdirSync12(projectPath, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.toLowerCase() !== root).map((entry) => entry.name).filter((name) => name.toLowerCase().startsWith("test") || root.startsWith(name.toLowerCase()));
38191
38478
  } catch {
38192
38479
  return "";
38193
38480
  }
@@ -38279,23 +38566,23 @@ function engineChecked(why) {
38279
38566
  var EXTERNAL_MEMBER = 'Could not resolve external class member "';
38280
38567
  function isDirectory(path) {
38281
38568
  try {
38282
- return statSync12(path).isDirectory();
38569
+ return statSync13(path).isDirectory();
38283
38570
  } catch {
38284
38571
  return false;
38285
38572
  }
38286
38573
  }
38287
38574
  function scriptsUnder(directory, prefix) {
38288
- if (existsSync20(join26(directory, ".gdignore"))) {
38575
+ if (existsSync21(join27(directory, ".gdignore"))) {
38289
38576
  return [];
38290
38577
  }
38291
38578
  const found = [];
38292
- for (const entry of readdirSync11(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
38579
+ for (const entry of readdirSync12(directory, { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : 1)) {
38293
38580
  if (entry.name.startsWith(".")) {
38294
38581
  continue;
38295
38582
  }
38296
38583
  const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
38297
38584
  if (entry.isDirectory()) {
38298
- found.push(...scriptsUnder(join26(directory, entry.name), path));
38585
+ found.push(...scriptsUnder(join27(directory, entry.name), path));
38299
38586
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
38300
38587
  found.push(path);
38301
38588
  }
@@ -38311,7 +38598,7 @@ function runtimeAddonAnnouncesEditors(projectPath) {
38311
38598
  return false;
38312
38599
  }
38313
38600
  try {
38314
- return readFileSync24(join26(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
38601
+ return readFileSync24(join27(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
38315
38602
  } catch {
38316
38603
  return false;
38317
38604
  }
@@ -38337,7 +38624,7 @@ var STATIC_LIST_HINT = { ttlMs: 5 * 60000, cacheScope: "private" };
38337
38624
  class GodotServer {
38338
38625
  stdio = null;
38339
38626
  locator = new GodotLocator;
38340
- operationsScript = join26(__dirname2, "godot", "operations", "godot_operations.gd");
38627
+ operationsScript = join27(__dirname2, "godot", "operations", "godot_operations.gd");
38341
38628
  godotBridge;
38342
38629
  tools = buildToolDefinitions();
38343
38630
  activeProcess = null;
@@ -38364,6 +38651,7 @@ class GodotServer {
38364
38651
  inFlight = new Map;
38365
38652
  callsOnGames = new Set;
38366
38653
  callsTaken = 0;
38654
+ answersDirectory;
38367
38655
  clientNoted = null;
38368
38656
  parentCommand = null;
38369
38657
  endNoted = false;
@@ -38597,14 +38885,14 @@ class GodotServer {
38597
38885
  }
38598
38886
  noteTheStart() {
38599
38887
  const cwd = process.cwd();
38600
- const mcpJson = join26(cwd, ".mcp.json");
38888
+ const mcpJson = join27(cwd, ".mcp.json");
38601
38889
  const announced = this.ownProject === null ? null : readAnnouncement(announcementPath(this.ownProject));
38602
38890
  const predecessor = announced !== null && announced.pid !== process.pid && alive(announced.pid) ? announced.pid : null;
38603
38891
  writeLifecycle("started", {
38604
38892
  argv: process.argv.slice(1),
38605
38893
  cwd,
38606
38894
  project: this.ownProject,
38607
- mcpJson: existsSync20(mcpJson) ? mcpJson : null
38895
+ mcpJson: existsSync21(mcpJson) ? mcpJson : null
38608
38896
  });
38609
38897
  if (predecessor !== null) {
38610
38898
  writeLifecycle("replaced", { project: this.ownProject, predecessor });
@@ -38745,7 +39033,7 @@ class GodotServer {
38745
39033
  return checked.response;
38746
39034
  }
38747
39035
  const named = args["projectPath"];
38748
- if (typeof named === "string" && existsSync20(join26(named, "project.godot"))) {
39036
+ if (typeof named === "string" && existsSync21(join27(named, "project.godot"))) {
38749
39037
  this.lastProjectPath = named;
38750
39038
  }
38751
39039
  this.updates.refresh();
@@ -38762,12 +39050,66 @@ class GodotServer {
38762
39050
  this.inFlight.set(call, { tool: spec.name, op: checked.op ?? "", startedAt: Date.now() });
38763
39051
  try {
38764
39052
  const answer = await withCallSignal(ctx.mcpReq.signal, () => this.answered(spec.name, checked.op ?? "", args), progress);
38765
- return this.withFeedbackNotice(this.withUpdateNotice(answer));
39053
+ return this.withinTheClientsLimit(spec.name, this.withFeedbackNotice(this.withUpdateNotice(answer)));
38766
39054
  } finally {
38767
39055
  this.inFlight.delete(call);
38768
39056
  }
38769
39057
  });
38770
39058
  }
39059
+ withinTheClientsLimit(tool, answer) {
39060
+ const sizeOf = (item) => item.type === "text" ? Buffer.byteLength(item.text) : item.data.length;
39061
+ const bytes = answer.content.reduce((sum, item) => sum + sizeOf(item), 0);
39062
+ if (bytes <= ANSWER_BUDGET) {
39063
+ return answer;
39064
+ }
39065
+ this.answersDirectory ??= scratchDirectory("answers");
39066
+ const directory = this.answersDirectory;
39067
+ const stem = `${tool}-${Date.now()}`;
39068
+ let savedAt;
39069
+ let notSaved;
39070
+ try {
39071
+ savedAt = answer.content.map((item, index) => {
39072
+ const file = join27(directory, item.type === "text" ? `${stem}-${index}.txt` : `${stem}-${index}.${item.mimeType.split("/")[1] ?? "bin"}`);
39073
+ writeFileSync16(file, item.type === "text" ? item.text : Buffer.from(item.data, "base64"));
39074
+ return file;
39075
+ });
39076
+ } catch (error) {
39077
+ notSaved = errorMessage(error);
39078
+ }
39079
+ const first = answer.content[0];
39080
+ let parsed = null;
39081
+ try {
39082
+ parsed = first?.type === "text" ? JSON.parse(first.text) : null;
39083
+ } catch {}
39084
+ const small = {};
39085
+ const leftOut = [];
39086
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
39087
+ for (const [key, value] of Object.entries(parsed)) {
39088
+ if (JSON.stringify(value).length <= 2000) {
39089
+ small[key] = value;
39090
+ } else {
39091
+ leftOut.push(key);
39092
+ }
39093
+ }
39094
+ }
39095
+ return {
39096
+ ...answer,
39097
+ content: [
39098
+ {
39099
+ type: "text",
39100
+ text: answerJson({
39101
+ ...small,
39102
+ answerTooLarge: {
39103
+ bytes,
39104
+ savedAt,
39105
+ leftOut: leftOut.length > 0 ? leftOut : undefined,
39106
+ note: savedAt === undefined ? `The answer was ${bytes} bytes, past the ${ANSWER_BUDGET} this server sends in one message (Claude Code closes the connection past 16 MB), and writing it to a file failed: ${notSaved ?? "no reason given"}. The fields of its top level that are short are kept here.` : `The answer was ${bytes} bytes, past the ${ANSWER_BUDGET} this server sends in one message (Claude Code closes the connection past 16 MB), so it was written whole to savedAt; read it from there. The fields of its top level that are short are kept here. The files are removed once this server has stopped.`
39107
+ }
39108
+ })
39109
+ }
39110
+ ]
39111
+ };
39112
+ }
38771
39113
  async answered(tool, op, args) {
38772
39114
  try {
38773
39115
  return await this.dispatch(tool, op, args);
@@ -39019,6 +39361,7 @@ class GodotServer {
39019
39361
  const contained = this.containProjectFiles({ ...arguments_, ...extra });
39020
39362
  return contained.ok ? await this.handleViaBridge(command, contained.value, op === "" ? tool : `${tool} ${op}`) : contained.response;
39021
39363
  };
39364
+ const editorOrEngine = async (command, effect, extra = {}) => await this.editorOrEngine(command, { ...arguments_, ...extra }, `${tool} ${op}`, effect);
39022
39365
  switch (tool) {
39023
39366
  case "project_info":
39024
39367
  return await this.handleProjectInfo(args);
@@ -39031,7 +39374,7 @@ class GodotServer {
39031
39374
  case "project_test":
39032
39375
  return await this.handleRunTests(args);
39033
39376
  case "scene_create":
39034
- return op === "create" ? await bridge("create_scene") : await bridge("save_scene");
39377
+ return op === "create" ? await editorOrEngine("create_scene", "writes") : await editorOrEngine("save_scene", "writes");
39035
39378
  case "scene_tree":
39036
39379
  return await this.readFromFile(args, "scenePath", "read_scene_tree", (file) => ({
39037
39380
  scenePath: file,
@@ -39041,28 +39384,28 @@ class GodotServer {
39041
39384
  case "scene_node":
39042
39385
  switch (op) {
39043
39386
  case "add":
39044
- return await bridge("add_node");
39387
+ return await editorOrEngine("add_node", "writes");
39045
39388
  case "get":
39046
- return await bridge("get_node_properties");
39389
+ return await editorOrEngine("get_node_properties", "reads");
39047
39390
  case "set":
39048
- return await bridge("set_node_properties");
39391
+ return await editorOrEngine("set_node_properties", "writes");
39049
39392
  case "duplicate":
39050
- return await bridge("duplicate_node", { parentPath: args["parentNodePath"] });
39393
+ return await editorOrEngine("duplicate_node", "writes", { parentPath: args["parentNodePath"] });
39051
39394
  case "reparent":
39052
- return await bridge("reparent_node");
39395
+ return await editorOrEngine("reparent_node", "writes");
39053
39396
  case "delete":
39054
- return await bridge("delete_node");
39397
+ return await editorOrEngine("delete_node", "writes");
39055
39398
  default:
39056
- return await bridge("set_tilemap_cells", { tilemapNodePath: args["nodePath"] });
39399
+ return await editorOrEngine("set_tilemap_cells", "writes", { tilemapNodePath: args["nodePath"] });
39057
39400
  }
39058
39401
  case "scene_signal":
39059
39402
  switch (op) {
39060
39403
  case "connect":
39061
- return await bridge("connect_signal");
39404
+ return await editorOrEngine("connect_signal", "writes");
39062
39405
  case "disconnect":
39063
- return await bridge("disconnect_signal");
39406
+ return await editorOrEngine("disconnect_signal", "writes");
39064
39407
  default:
39065
- return await bridge("list_connections");
39408
+ return await editorOrEngine("list_connections", "reads");
39066
39409
  }
39067
39410
  case "scene_animation":
39068
39411
  switch (op) {
@@ -39113,17 +39456,17 @@ class GodotServer {
39113
39456
  }), async () => await bridge("read_resource", kept));
39114
39457
  }
39115
39458
  case "create":
39116
- return await bridge("create_resource");
39459
+ return await editorOrEngine("create_resource", "writes");
39117
39460
  case "modify":
39118
- return await bridge("modify_resource");
39461
+ return await editorOrEngine("modify_resource", "writes");
39119
39462
  case "create_shader":
39120
- return await bridge("create_shader", { shaderPath: args["resourcePath"] });
39463
+ return await editorOrEngine("create_shader", "writes", { shaderPath: args["resourcePath"] });
39121
39464
  case "create_tileset":
39122
- return await bridge("create_tileset", { tilesetPath: args["resourcePath"] });
39465
+ return await editorOrEngine("create_tileset", "writes", { tilesetPath: args["resourcePath"] });
39123
39466
  case "set_theme_color":
39124
- return await bridge("set_theme_color", { themePath: args["resourcePath"] });
39467
+ return await editorOrEngine("set_theme_color", "writes", { themePath: args["resourcePath"] });
39125
39468
  default:
39126
- return await bridge("set_theme_font_size", { themePath: args["resourcePath"] });
39469
+ return await editorOrEngine("set_theme_font_size", "writes", { themePath: args["resourcePath"] });
39127
39470
  }
39128
39471
  case "editor_launch":
39129
39472
  if (op === "close") {
@@ -39344,8 +39687,8 @@ class GodotServer {
39344
39687
  if (path === undefined) {
39345
39688
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
39346
39689
  }
39347
- const file = join26(path, "project.godot");
39348
- if (!existsSync20(file)) {
39690
+ const file = join27(path, "project.godot");
39691
+ if (!existsSync21(file)) {
39349
39692
  return {
39350
39693
  ok: false,
39351
39694
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -39391,7 +39734,7 @@ class GodotServer {
39391
39734
  }
39392
39735
  runEnvironment(args) {
39393
39736
  const savesIn = readNonEmptyString(args, "savesIn");
39394
- if (savesIn !== undefined && !isAbsolute4(savesIn)) {
39737
+ if (savesIn !== undefined && !isAbsolute5(savesIn)) {
39395
39738
  return {
39396
39739
  ok: false,
39397
39740
  response: this.createErrorResponse(`savesIn must be an absolute path, not "${savesIn}".`, [
@@ -39650,7 +39993,8 @@ class GodotServer {
39650
39993
  uidsCreated: given,
39651
39994
  stillWithoutUid: after,
39652
39995
  note: uidsLeftNote(after.length),
39653
- ...imported.extensionNote === undefined ? {} : { extensionNote: imported.extensionNote }
39996
+ ...imported.extensionNote === undefined ? {} : { extensionNote: imported.extensionNote },
39997
+ ...imported.docCacheNote === undefined ? {} : { docCacheNote: imported.docCacheNote }
39654
39998
  }
39655
39999
  }, scanned));
39656
40000
  }
@@ -39889,6 +40233,9 @@ class GodotServer {
39889
40233
  if (imported.extensionNote !== undefined) {
39890
40234
  extra["extensionNote"] = imported.extensionNote;
39891
40235
  }
40236
+ if (imported.docCacheNote !== undefined) {
40237
+ extra["docCacheNote"] = imported.docCacheNote;
40238
+ }
39892
40239
  }
39893
40240
  const after = await this.operation("get_import_status", statusAsk, projectPath);
39894
40241
  if (!after.ok) {
@@ -39989,6 +40336,10 @@ class GodotServer {
39989
40336
  if (unknown.length > 0) {
39990
40337
  return this.createErrorResponse(`project_info cannot include ${unknown.join(", ")}. Sections: ${Object.keys(PROJECT_INFO_SECTIONS2).join(", ")}.`);
39991
40338
  }
40339
+ const forValidation = ["preset", "detail"].filter((name) => args[name] !== undefined);
40340
+ if (forValidation.length > 0 && !include.includes("validation")) {
40341
+ return this.createErrorResponse(`${forValidation.join(" and ")} ${forValidation.length === 1 ? "is" : "are"} read only by the validation section, which this call does not include: add "validation" to include, or leave ${forValidation.length === 1 ? "it" : "them"} out.`);
40342
+ }
39992
40343
  const project = this.project(args);
39993
40344
  if (!project.ok) {
39994
40345
  return project.response;
@@ -40001,7 +40352,7 @@ class GodotServer {
40001
40352
  const name = application["config/name"];
40002
40353
  const mainScene = application["run/main_scene"];
40003
40354
  const info = {
40004
- name: typeof name === "string" && name !== "" ? name : basename7(project.value.path),
40355
+ name: typeof name === "string" && name !== "" ? name : basename8(project.value.path),
40005
40356
  path: project.value.path,
40006
40357
  mainScene: typeof mainScene === "string" && mainScene !== "" ? mainScene : null,
40007
40358
  godotVersion: await this.godotVersion(engine.value),
@@ -40084,18 +40435,18 @@ class GodotServer {
40084
40435
  if (!engine.ok) {
40085
40436
  return engine.response;
40086
40437
  }
40087
- const folder = dirname18(output.absolutePath);
40438
+ const folder = dirname19(output.absolutePath);
40088
40439
  try {
40089
40440
  mkdirSync15(folder, { recursive: true });
40090
40441
  } catch (error) {
40091
- return this.createErrorResponse(`Cannot create ${dirname18(output.relativePath)} to export into: ${errorMessage(error)}`);
40442
+ return this.createErrorResponse(`Cannot create ${dirname19(output.relativePath)} to export into: ${errorMessage(error)}`);
40092
40443
  }
40093
40444
  const debug = readBoolean(args, "debug") ?? false;
40094
40445
  const exportLogs = scratchDirectory("export");
40095
40446
  const exportArgs = [
40096
40447
  "--headless",
40097
40448
  "--log-file",
40098
- join26(exportLogs, "engine.log"),
40449
+ join27(exportLogs, "engine.log"),
40099
40450
  "--path",
40100
40451
  project.value.path,
40101
40452
  debug ? "--export-debug" : "--export-release",
@@ -40103,24 +40454,37 @@ class GodotServer {
40103
40454
  output.absolutePath
40104
40455
  ];
40105
40456
  this.logDebug(`Exporting: ${engine.value} ${exportArgs.join(" ")}`);
40106
- const writtenAt = () => existsSync20(output.absolutePath) ? statSync12(output.absolutePath).mtimeMs : null;
40107
- const before = writtenAt();
40108
- const listedBefore = filesIn(folder);
40109
- const freed = freeLibraryCopies(project.value.path);
40110
- if (freed.setAside.length > 0 || freed.swept.length > 0) {
40111
- this.logDebug(`Before the export: set aside ${freed.setAside.join(", ")}; swept ${freed.swept.join(", ")}`);
40112
- }
40113
- let ending;
40457
+ const writtenAt = () => existsSync21(output.absolutePath) ? statSync13(output.absolutePath).mtimeMs : null;
40458
+ const caches = editorCacheDirectory(engine.value);
40459
+ const exportOnce = async () => {
40460
+ const before = writtenAt();
40461
+ const listedBefore = filesIn(folder);
40462
+ const cachesBefore = docCachesWritten(caches);
40463
+ const freed = freeLibraryCopies(project.value.path);
40464
+ if (freed.setAside.length > 0 || freed.swept.length > 0) {
40465
+ this.logDebug(`Before the export: set aside ${freed.setAside.join(", ")}; swept ${freed.swept.join(", ")}`);
40466
+ }
40467
+ const ending = await runEngine(engine.value, exportArgs, {
40468
+ timeout: { ms: 300000, said: "five minutes" }
40469
+ });
40470
+ const crash = docCacheCrash(ending, cachesBefore, docCachesWritten(caches), "export");
40471
+ return { ending, before, listedBefore, crash };
40472
+ };
40473
+ let run;
40474
+ let docCacheNote;
40114
40475
  try {
40115
- ending = await runEngine(engine.value, exportArgs, { timeout: { ms: 300000, said: "five minutes" } });
40476
+ const first = await exportOnce();
40477
+ docCacheNote = first.crash;
40478
+ run = first.crash === null ? first : await exportOnce();
40116
40479
  } finally {
40117
40480
  discard(exportLogs);
40118
40481
  }
40482
+ const { ending, before, listedBefore } = run;
40119
40483
  if (ending.exitSignal === null && ending.exitCode === null) {
40120
40484
  return this.createErrorResponse(`Export could not be run: ${ending.failure ?? "the engine gave no exit status"}`);
40121
40485
  }
40122
- const beside = writtenBeside(listedBefore, filesIn(folder), basename7(output.absolutePath), dirname18(output.relativePath).replaceAll("\\", "/"));
40123
- return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt(), beside);
40486
+ const beside = writtenBeside(listedBefore, filesIn(folder), basename8(output.absolutePath), dirname19(output.relativePath).replaceAll("\\", "/"));
40487
+ return exportAnswer(ending, { preset, outputPath: output.relativePath, debug }, before, writtenAt(), beside, docCacheNote);
40124
40488
  }
40125
40489
  reportTestProgress(log) {
40126
40490
  const progress = callProgress();
@@ -40173,10 +40537,10 @@ class GodotServer {
40173
40537
  if (movie !== null) {
40174
40538
  return this.createErrorResponse(movie);
40175
40539
  }
40176
- const missing = paths.value.filter((path) => !existsSync20(join26(project.value.path, path.slice("res://".length))));
40540
+ const missing = paths.value.filter((path) => !existsSync21(join27(project.value.path, path.slice("res://".length))));
40177
40541
  const asked = paths.value.filter((path) => !missing.includes(path));
40178
40542
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
40179
- const noRunner = !existsSync20(join26(project.value.path, runner));
40543
+ const noRunner = !existsSync21(join27(project.value.path, runner));
40180
40544
  if (asked.length === 0) {
40181
40545
  const verdict = `nothing at ${missing.join(" and ")}`;
40182
40546
  const andNoRunner = noRunner ? `, and gdUnit4 is not installed in this project: no ${runner}` : "";
@@ -40267,21 +40631,21 @@ class GodotServer {
40267
40631
  await untilGone([run.process.pid]);
40268
40632
  }
40269
40633
  await run.settled;
40270
- const reportsDir = join26(project.value.path, ".godot", ...ours.split("/"));
40634
+ const reportsDir = join27(project.value.path, ".godot", ...ours.split("/"));
40271
40635
  let report = null;
40272
40636
  let reportProblem = null;
40273
40637
  try {
40274
- const written = existsSync20(reportsDir) ? readdirSync11(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
40638
+ const written = existsSync21(reportsDir) ? readdirSync12(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
40275
40639
  const newest = written.at(-1);
40276
40640
  if (newest !== undefined) {
40277
- report = parseJUnit(readFileSync24(join26(reportsDir, newest, "results.xml"), "utf8"));
40641
+ report = parseJUnit(readFileSync24(join27(reportsDir, newest, "results.xml"), "utf8"));
40278
40642
  }
40279
40643
  } catch (error) {
40280
40644
  reportProblem = errorMessage(error);
40281
40645
  } finally {
40282
40646
  discard(reportsDir, userData);
40283
40647
  try {
40284
- rmdirSync2(dirname18(reportsDir));
40648
+ rmdirSync2(dirname19(reportsDir));
40285
40649
  } catch {}
40286
40650
  }
40287
40651
  const engineEntries = foldedRepeats(run.log.select({
@@ -40313,7 +40677,7 @@ class GodotServer {
40313
40677
  const declaring = printedErrors.some((error) => unresolvedClassIn(error.message) !== null) ? await originsOf(printedErrors, declaredClasses(project.value.path), checkWithEngine(engine.value, project.value.path)) : { origins: [], unconfirmed: [] };
40314
40678
  const scriptErrors = [...declaring.origins, ...printedErrors];
40315
40679
  const brokenScripts = new Set(scriptErrors.map((error) => error.path)).size;
40316
- const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (uncaught.length > 0 ? `${uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`} that failed no case` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
40680
+ const ranAs = cutShort !== null ? cutShort.verdict : nothingRan ?? (brokenScripts > 0 ? `script errors in ${brokenScripts === 1 ? "one script" : `${brokenScripts} scripts`}, so no suite ran` : undefined) ?? (uncaught.length > 0 ? `${uncaught.length === 1 ? "one script error" : `${uncaught.length} script errors`} that failed no case` : undefined) ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? howItExited(run);
40317
40681
  const nothingAt = missing.join(" and ");
40318
40682
  const verdict = missing.length === 0 ? ranAs : `${ranAs}; nothing at ${nothingAt}`;
40319
40683
  const missingNote = missing.length === 0 ? undefined : `Nothing at ${nothingAt}, so ${missing.length === 1 ? "it was" : "they were"} left out and only ${asked.join(", ")} ran; a run of fewer paths than were asked for is not called a pass.${elsewhereIn(project.value.path, missing[0] ?? "")}`;
@@ -40373,7 +40737,7 @@ class GodotServer {
40373
40737
  isError: true
40374
40738
  };
40375
40739
  }
40376
- const failed = failedCases(report, Buffer.concat(console2).toString("utf8"));
40740
+ const { failed, briefed } = failedWithin(failedCases(report, Buffer.concat(console2).toString("utf8")), FAILED_BUDGET);
40377
40741
  const orphans = orphansPrinted(said);
40378
40742
  const leftBehind = new Map(orphans.suites.map((suite) => [suite.path, suite.orphans]));
40379
40743
  const orphansIn = (path) => path === null ? 0 : leftBehind.get(path) ?? 0;
@@ -40413,6 +40777,7 @@ class GodotServer {
40413
40777
  skipped: report.skipped,
40414
40778
  time: report.time,
40415
40779
  failed,
40780
+ failedWithoutDetail: briefed > 0 ? briefed : undefined,
40416
40781
  hookFailures: hookFailures.length > 0 ? hookFailures : undefined,
40417
40782
  uncaughtScriptErrors: uncaughtFolded.length > 0 ? uncaughtFolded : undefined,
40418
40783
  warnings: warnings.length > 0 ? warnings : undefined,
@@ -40424,6 +40789,7 @@ class GodotServer {
40424
40789
  ...scanned,
40425
40790
  note: [
40426
40791
  missingNote,
40792
+ briefed > 0 ? `${briefed} of the ${failed.length} failing cases, those with the largest reports, are named with their first message only, since their reports would have made the answer too large to deliver; pass one case's suite path to see its reports.` : undefined,
40427
40793
  notApplied.length > 0 ? "An ignore entry named a case that ran all the same: gdUnit4 leaves a case out only as suite_name:test_name, and reads an entry with no colon as a suite name or a piece of a suite path. ignoreNotApplied gives the form for each case that ran." : undefined,
40428
40794
  uncaughtNote(uncaught),
40429
40795
  ranAs.startsWith("warnings") && warnings.length === 0 ? "gdUnit4 exits 101 for orphan nodes when nothing failed, and this run printed no count of them: orphan reporting may be off in the project settings." : notRunNote(notRun, readBoolean(args, "failFast") === true)
@@ -40587,7 +40953,11 @@ class GodotServer {
40587
40953
  };
40588
40954
  }
40589
40955
  markedIfStale(answer) {
40590
- return markIfStale(answer, this.godotBridge.getStatus().addonVersion, SERVER_VERSION, this.godotBridge.getStatus().addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
40956
+ const status = this.godotBridge.getStatus();
40957
+ if (!hasSaidWhoItIs(status)) {
40958
+ return answer;
40959
+ }
40960
+ return markIfStale(answer, status.addonVersion, SERVER_VERSION, status.addonDigest, shippedEditorDigest(), this.editorCodeOnDisk());
40591
40961
  }
40592
40962
  async diagnosticsOf(args) {
40593
40963
  const waited = await this.waitForTheEditorOpenedHere(args, []);
@@ -40634,7 +41004,7 @@ class GodotServer {
40634
41004
  const cached = cachedClasses(projectPath);
40635
41005
  const sourceOf = (resourcePath) => {
40636
41006
  const contained = resolveWithinProject(projectPath, resourcePath);
40637
- if (!contained.ok || !existsSync20(contained.absolutePath)) {
41007
+ if (!contained.ok || !existsSync21(contained.absolutePath)) {
40638
41008
  return null;
40639
41009
  }
40640
41010
  try {
@@ -40728,7 +41098,7 @@ class GodotServer {
40728
41098
  }
40729
41099
  sourceOfClass(projectPath, resourcePath) {
40730
41100
  const contained = resolveWithinProject(projectPath, resourcePath);
40731
- if (!contained.ok || !existsSync20(contained.absolutePath)) {
41101
+ if (!contained.ok || !existsSync21(contained.absolutePath)) {
40732
41102
  return null;
40733
41103
  }
40734
41104
  try {
@@ -40890,7 +41260,7 @@ class GodotServer {
40890
41260
  ...Array.isArray(stack) ? {
40891
41261
  stack: stack.map((frame) => ({
40892
41262
  ...frame,
40893
- ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join26(projectPath, frame["script"].slice("res://".length)) } : {}
41263
+ ...projectPath !== "" && typeof frame["script"] === "string" && frame["script"].startsWith("res://") ? { path: join27(projectPath, frame["script"].slice("res://".length)) } : {}
40894
41264
  }))
40895
41265
  } : {}
40896
41266
  });
@@ -41169,7 +41539,7 @@ class GodotServer {
41169
41539
  return null;
41170
41540
  }
41171
41541
  try {
41172
- return settingKeys(readFileSync24(join26(projectPath, "project.godot"), "utf8"));
41542
+ return settingKeys(readFileSync24(join27(projectPath, "project.godot"), "utf8"));
41173
41543
  } catch {
41174
41544
  return null;
41175
41545
  }
@@ -41408,7 +41778,7 @@ class GodotServer {
41408
41778
  return;
41409
41779
  }
41410
41780
  const connected = this.godotBridge.isConnected() ? this.godotBridge.getStatus().editorPid : undefined;
41411
- const pids = editorsOfProjectIn(tree, projectPath).filter((pid) => pid !== connected);
41781
+ const pids = editorsOfProjectIn(tree, projectPath).filter((pid) => pid !== connected && alive(pid));
41412
41782
  const started = await startTimesOf(pids);
41413
41783
  return pids.map((pid) => {
41414
41784
  const at = started.get(pid) ?? tree.get(pid)?.startedAt;
@@ -41606,7 +41976,7 @@ class GodotServer {
41606
41976
  }
41607
41977
  async runtimeUp(projectPath, before, budget, withArgs) {
41608
41978
  const budgetMs = budget.ms;
41609
- if (!existsSync20(join26(projectPath, RUNTIME_AUTOLOAD.path))) {
41979
+ if (!existsSync21(join27(projectPath, RUNTIME_AUTOLOAD.path))) {
41610
41980
  return runtimeVerdict(null, {
41611
41981
  addon: false,
41612
41982
  budgetMs,
@@ -41692,7 +42062,7 @@ class GodotServer {
41692
42062
  }
41693
42063
  let announcedAt;
41694
42064
  try {
41695
- announcedAt = statSync12(endpoint.file).mtimeMs;
42065
+ announcedAt = statSync13(endpoint.file).mtimeMs;
41696
42066
  } catch {
41697
42067
  return;
41698
42068
  }
@@ -42582,9 +42952,13 @@ class GodotServer {
42582
42952
  if (callersName !== undefined && !callersName.replace(/@([^.@]+)_(?:getter|setter)$/, "$1").split(".").every(isIdentifier)) {
42583
42953
  return this.createErrorResponse(`callersOf takes a function's name, or Class.name for the functions of one class, and ${JSON.stringify(callersName)} is neither.`, ["editor_run profile names each function as it answers; pass one of those"]);
42584
42954
  }
42955
+ const asked = {
42956
+ mark: readBoolean(args, "mark") === true,
42957
+ since: readString(args, "since") === "mark"
42958
+ };
42585
42959
  const going = await this.runStillGoing(run);
42586
42960
  const projectPath = run.projectPath;
42587
- const projectGone = projectPath !== null && !existsSync20(join26(projectPath, "project.godot"));
42961
+ const projectGone = projectPath !== null && !existsSync21(join27(projectPath, "project.godot"));
42588
42962
  if (projectGone && callersName !== undefined) {
42589
42963
  return this.createErrorResponse(`callersOf reads the project's scripts, and the run's project at ${projectPath} has no project.godot any more.`, ["editor_run profile without callersOf answers with the profile itself"]);
42590
42964
  }
@@ -42594,7 +42968,7 @@ class GodotServer {
42594
42968
  ...callersName === undefined ? {} : { callersOf: { name: callersName, scripts: () => projectScripts(projectPath) } }
42595
42969
  };
42596
42970
  if (run.throughEditor) {
42597
- return await this.profileOfThePlayedGame(going, projectPath, fromProject, limit);
42971
+ return await this.profileOfThePlayedGame(run, going, fromProject, limit, asked);
42598
42972
  }
42599
42973
  const path = run.profile;
42600
42974
  if (path === undefined) {
@@ -42607,15 +42981,16 @@ class GodotServer {
42607
42981
  ]);
42608
42982
  }
42609
42983
  const totals = file.connected ? file : null;
42610
- return this.jsonTextResponse(profileAnswer({
42984
+ return this.profileReply(run, {
42611
42985
  through: "gdharness",
42612
42986
  running: going,
42613
42987
  totals,
42614
42988
  coveredMs: totals?.lastFrameAt == null ? 0 : totals.lastFrameAt - totals.startedAt,
42615
42989
  ...fromProject
42616
- }, limit));
42990
+ }, limit, asked);
42617
42991
  }
42618
- async profileOfThePlayedGame(going, projectPath, fromProject, limit) {
42992
+ async profileOfThePlayedGame(run, going, fromProject, limit, asked) {
42993
+ const { projectPath } = run;
42619
42994
  const read = await this.handleViaBridge("profile_read", {});
42620
42995
  if (read.isError === true) {
42621
42996
  return read;
@@ -42637,12 +43012,13 @@ class GodotServer {
42637
43012
  return this.createErrorResponse("The editor says it is playing and has no debugger session for the game, so there is nothing to switch the profiler on in yet.", ["Ask again in a moment, once the game is up"]);
42638
43013
  }
42639
43014
  const on = await this.firstProfiledFrame();
43015
+ const unmarked = asked.mark || asked.since ? " Nothing was marked, and nothing is counted from a mark: the profile begins now, so an answer without since covers the same span." : "";
42640
43016
  return this.jsonTextResponse({
42641
43017
  through: "editor",
42642
43018
  running: true,
42643
43019
  started: true,
42644
43020
  functions: [],
42645
- note: on ? "The profiler was switched on now, for the game the editor is playing, and its first frame has arrived, so it covers what the game does from here on. Ask again for the functions; editor_run start with profile: true covers a game from its start." : `The profiler was switched on for the game the editor is playing, and no frame of it had arrived ${PROFILER_ON_MS}ms later: what the game does before the first one is not counted. Ask again for the functions; editor_run start with profile: true covers a game from its start.`
43021
+ note: on ? `The profiler was switched on now, for the game the editor is playing, and its first frame has arrived, so it covers what the game does from here on. Ask again for the functions; editor_run start with profile: true covers a game from its start.${unmarked}` : `The profiler was switched on for the game the editor is playing, and no frame of it had arrived ${PROFILER_ON_MS}ms later: what the game does before the first one is not counted. Ask again for the functions; editor_run start with profile: true covers a game from its start.${unmarked}`
42646
43022
  });
42647
43023
  }
42648
43024
  const functions = asParams(held["functions"]);
@@ -42665,13 +43041,70 @@ class GodotServer {
42665
43041
  ];
42666
43042
  }))
42667
43043
  } : null;
42668
- return this.jsonTextResponse(profileAnswer({
43044
+ return this.profileReply(run, {
42669
43045
  through: "editor",
42670
43046
  running: going,
42671
43047
  totals,
42672
43048
  coveredMs: readNonNegativeNumber(held, "coveredMs") ?? 0,
42673
43049
  ...fromProject
42674
- }, limit));
43050
+ }, limit, asked);
43051
+ }
43052
+ profileReply(run, reading, limit, asked) {
43053
+ const markPath = profileMarkPath(run.startedAt);
43054
+ const now = Date.now();
43055
+ let shown = reading;
43056
+ const lead = [];
43057
+ let sinceFields = {};
43058
+ if (asked.since) {
43059
+ const mark = readProfileMark(markPath);
43060
+ if (mark === null || mark.runStartedAt !== run.startedAt || mark.projectPath !== run.projectPath) {
43061
+ return this.createErrorResponse("No mark has been set in this run, so there is nothing to count from.", ["editor_run profile with mark: true sets one at the totals it answers with"]);
43062
+ }
43063
+ const added = reading.totals === null ? null : totalsSince(reading.totals, mark.totals);
43064
+ if (added === null) {
43065
+ return this.createErrorResponse("The profile holds less than it did at the mark, so the profiler was started again since, and what it holds now cannot be counted from the mark.", ["editor_run profile with mark: true sets a mark in the profile as it is now"]);
43066
+ }
43067
+ shown = {
43068
+ ...reading,
43069
+ totals: added.totals,
43070
+ coveredMs: Math.max(0, reading.coveredMs - mark.coveredMs)
43071
+ };
43072
+ const ago = now - mark.takenAt;
43073
+ sinceFields = {
43074
+ since: "mark",
43075
+ markSetMsAgo: ago,
43076
+ ...added.shrunk > 0 ? { lessThanAtMark: added.shrunk } : {}
43077
+ };
43078
+ lead.push(`Counted from the mark set ${ago}ms ago: each function's calls and milliseconds are what it added after the mark, frames and coveredMs are those since it, and a function that added no call is left out.`);
43079
+ if (added.shrunk > 0) {
43080
+ const one = added.shrunk === 1;
43081
+ lead.push(`${one ? "One function reads" : `${added.shrunk} functions read`} less than at the mark and ${one ? "is" : "are"} left out, counted under lessThanAtMark: the engine's own totals arrived after the mark and replaced the frames' sum, and capped totals leave out the functions with the least time.`);
43082
+ }
43083
+ }
43084
+ if (asked.mark) {
43085
+ if (reading.totals === null) {
43086
+ return this.createErrorResponse("There is nothing to mark yet: the game has not connected to the profiler.", ["Ask again in a moment, once the game is up"]);
43087
+ }
43088
+ const mark = {
43089
+ takenAt: now,
43090
+ projectPath: run.projectPath,
43091
+ runStartedAt: run.startedAt,
43092
+ coveredMs: reading.coveredMs,
43093
+ totals: reading.totals
43094
+ };
43095
+ if (!writeProfileMark(markPath, mark)) {
43096
+ return this.createErrorResponse(`The mark could not be written to ${markPath}, so none is set.`);
43097
+ }
43098
+ lead.push(asked.since ? 'The mark is moved to the totals as they are now, so the next since: "mark" counts from here.' : 'The mark is set at these totals: editor_run profile with since: "mark" answers only what is added after it.');
43099
+ }
43100
+ const answer = profileAnswer(shown, limit);
43101
+ const note = [...lead, ...typeof answer["note"] === "string" ? [answer["note"]] : []].join(" ");
43102
+ return this.jsonTextResponse({
43103
+ ...answer,
43104
+ ...sinceFields,
43105
+ ...asked.mark ? { marked: true } : {},
43106
+ note: note === "" ? undefined : note
43107
+ });
42675
43108
  }
42676
43109
  async firstProfiledFrame() {
42677
43110
  const deadline = Date.now() + PROFILER_ON_MS;
@@ -42872,7 +43305,7 @@ class GodotServer {
42872
43305
  notes.push(endedWithoutACode({
42873
43306
  throughEditor: run.throughEditor,
42874
43307
  announced: announced !== undefined || run.announcedPid !== undefined,
42875
- couldAnnounce: run.projectPath !== null && existsSync20(join26(run.projectPath, RUNTIME_AUTOLOAD.path)),
43308
+ couldAnnounce: run.projectPath !== null && existsSync21(join27(run.projectPath, RUNTIME_AUTOLOAD.path)),
42876
43309
  endedHere: typeof run.endedHere === "string",
42877
43310
  pickedUp: run.pickedUp === true
42878
43311
  }));
@@ -43502,7 +43935,7 @@ class GodotServer {
43502
43935
  if (!scriptPath.ok) {
43503
43936
  return this.createErrorResponse(scriptPath.reason);
43504
43937
  }
43505
- if (!existsSync20(scriptPath.absolutePath)) {
43938
+ if (!existsSync21(scriptPath.absolutePath)) {
43506
43939
  return this.createErrorResponse(`Script file does not exist: ${scriptPath.absolutePath}`);
43507
43940
  }
43508
43941
  const asked = readNonEmptyString(args, "newScriptPath");
@@ -43647,6 +44080,45 @@ class GodotServer {
43647
44080
  ...engineExtras(outcome)
43648
44081
  });
43649
44082
  }
44083
+ async editorOrEngine(command, args, askedAs, effect) {
44084
+ const contained = this.containProjectFiles(args);
44085
+ if (!contained.ok) {
44086
+ return contained.response;
44087
+ }
44088
+ const project = this.project(contained.value);
44089
+ if (!project.ok) {
44090
+ return project.response;
44091
+ }
44092
+ const projectPath = project.value.path;
44093
+ const waited = await this.waitForTheEditorOpenedHere(contained.value, []);
44094
+ if (this.editorHasOpen(projectPath)) {
44095
+ return await this.handleViaBridge(command, contained.value, askedAs);
44096
+ }
44097
+ if (effect === "writes") {
44098
+ if (waited !== null) {
44099
+ return waited;
44100
+ }
44101
+ const arriving = await this.editorsNotYetConnected(projectPath);
44102
+ if (arriving !== undefined && arriving.length > 0) {
44103
+ const named = arriving.map((one) => `pid ${one.pid}`).join(", ");
44104
+ return this.createErrorResponse(`${askedAs} was not run: an editor of this project is running (${named}) and has not reached this server, and saving a scene or resource it holds would put back what a write made around it.`, [
44105
+ "editor_status names it under editorsNotYetConnected and says when it has connected; the write then goes through it",
44106
+ `If it never connects it is not reaching this server: end ${arriving.length === 1 ? "that pid" : "those pids"} and ask again`
44107
+ ]);
44108
+ }
44109
+ }
44110
+ const outcome = await this.operation("run_editor_tool", { command, arguments: contained.value }, projectPath);
44111
+ if (!outcome.ok) {
44112
+ return this.answer(outcome);
44113
+ }
44114
+ return this.jsonTextResponse({
44115
+ ...outcome.payload,
44116
+ ...effect === "reads" ? {
44117
+ readBy: "a headless engine, since no editor serves this project; unsaved changes in an editor are not seen"
44118
+ } : { writtenBy: "a headless engine, since no editor serves this project" },
44119
+ ...engineExtras(outcome)
44120
+ });
44121
+ }
43650
44122
  async handleMoveFile(given) {
43651
44123
  const project = this.project(given);
43652
44124
  if (!project.ok) {
@@ -44045,7 +44517,7 @@ class GodotServer {
44045
44517
  async ownRunOnItsWay(projectPath, budgetMs) {
44046
44518
  const run = await this.currentRun();
44047
44519
  const project = projectPath ?? run?.projectPath ?? null;
44048
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync20(join26(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
44520
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync21(join27(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
44049
44521
  return null;
44050
44522
  }
44051
44523
  const already = runtimesAnnounced().running;
@@ -44054,7 +44526,7 @@ class GodotServer {
44054
44526
  return !run.announcedBefore.has(one.pid);
44055
44527
  }
44056
44528
  try {
44057
- return statSync12(one.file).mtimeMs >= run.startedAt;
44529
+ return statSync13(one.file).mtimeMs >= run.startedAt;
44058
44530
  } catch {
44059
44531
  return false;
44060
44532
  }
@@ -44135,7 +44607,7 @@ class GodotServer {
44135
44607
  }
44136
44608
  }
44137
44609
  const screenshotDir = expectsScreenshot ? scratchDirectory("runtime-screenshot") : null;
44138
- const screenshotPath = screenshotDir ? join26(screenshotDir, "capture.png") : null;
44610
+ const screenshotPath = screenshotDir ? join27(screenshotDir, "capture.png") : null;
44139
44611
  const staleNote = this.runtimeStaleNote(choice.endpoint);
44140
44612
  const stale = staleNote === undefined ? {} : { addonIsStale: true, staleNote };
44141
44613
  const refused = (message) => this.createErrorResponse(staleNote === undefined ? message : `${message}