gdharness 1.3.45 → 1.3.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -68,6 +68,124 @@ var init_errors = __esm(() => {
68
68
  };
69
69
  });
70
70
 
71
+ // src/paths.ts
72
+ import { realpathSync, statSync } from "node:fs";
73
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
74
+ function realAsFarAsItExists(path) {
75
+ const missing = [];
76
+ let at = path;
77
+ for (;; ) {
78
+ try {
79
+ return join(realpathSync.native(at), ...missing);
80
+ } catch {
81
+ const parent = dirname(at);
82
+ if (parent === at) {
83
+ return path;
84
+ }
85
+ missing.unshift(basename(at));
86
+ at = parent;
87
+ }
88
+ }
89
+ }
90
+ function isSameDirectory(onePath, otherPath) {
91
+ return relative(realAsFarAsItExists(resolve(onePath)), realAsFarAsItExists(resolve(otherPath))) === "";
92
+ }
93
+ function isWithinRoot(rootPath, candidatePath) {
94
+ const step = relative(resolve(rootPath), resolve(candidatePath));
95
+ return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
96
+ }
97
+ function keptInside(given) {
98
+ const at = given.indexOf("::");
99
+ const inner = at === -1 ? "" : given.slice(at + 2);
100
+ return KEPT_ID.test(inner) ? { file: given.slice(0, at), inner } : { file: given, inner: undefined };
101
+ }
102
+ function resolveWithinProject(projectPath, candidatePath, options = {}) {
103
+ const trimmed = candidatePath.trim();
104
+ if (trimmed === "") {
105
+ return { ok: false, reason: "Path is empty." };
106
+ }
107
+ if (trimmed.includes("\x00")) {
108
+ return { ok: false, reason: "Path contains a null byte." };
109
+ }
110
+ const root = resolve(projectPath);
111
+ const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : isAbsolute(trimmed) && isWithinRoot(root, trimmed) ? relative(root, resolve(trimmed)).split(sep).join("/") : trimmed;
112
+ if (withoutScheme.startsWith("uid://")) {
113
+ return {
114
+ ok: false,
115
+ reason: `Path '${trimmed}' is a UID, and a file is judged here by where it is on disk, which a UID does not say. Give the file's res:// path.`
116
+ };
117
+ }
118
+ const scheme = OTHER_SCHEME.exec(withoutScheme);
119
+ if (scheme) {
120
+ const elsewhere = scheme[0].toLowerCase() === "user://" ? " A game's user:// is a directory of its own outside the project: a running game's is under userDir in editor_status runtimes and in editor_run start's runtime, where a file reader of your own reads it." : "";
121
+ return {
122
+ ok: false,
123
+ reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.${elsewhere}`
124
+ };
125
+ }
126
+ if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
127
+ const tail = withoutScheme.split(/[\\/]+/).filter((step) => step !== "" && !step.endsWith(":"));
128
+ const besideIt = isAbsolute(trimmed) && isWithinRoot(realAsFarAsItExists(dirname(resolve(trimmed))), realAsFarAsItExists(root));
129
+ const example = tail.slice(besideIt ? -1 : -2).join("/") || "scenes/main.tscn";
130
+ return {
131
+ ok: false,
132
+ reason: `Path '${trimmed}' is absolute and outside the project; give a path inside it, such as '${example}'.`
133
+ };
134
+ }
135
+ const kept = keptInside(trimmed);
136
+ if (kept.inner !== undefined) {
137
+ return {
138
+ ok: false,
139
+ reason: `Path '${trimmed}' names the resource ${kept.inner} kept inside ${kept.file}, not a file. resource_edit read reads one by that path; this argument takes a file.`
140
+ };
141
+ }
142
+ if (withoutScheme.includes(":")) {
143
+ return {
144
+ ok: false,
145
+ reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
146
+ };
147
+ }
148
+ const absolutePath = resolve(root, withoutScheme);
149
+ if (!isWithinRoot(root, absolutePath)) {
150
+ return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
151
+ }
152
+ const relativePath = relative(root, absolutePath).split(sep).join("/");
153
+ if (relativePath === "" && options.root !== true) {
154
+ return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
155
+ }
156
+ return { ok: true, absolutePath, relativePath };
157
+ }
158
+ function exportedBuild(projectPath, candidatePath) {
159
+ const found = resolveWithinProject(projectPath, candidatePath);
160
+ if (!found.ok) {
161
+ return found;
162
+ }
163
+ let real;
164
+ try {
165
+ real = realpathSync.native(found.absolutePath);
166
+ } catch {
167
+ return {
168
+ ok: false,
169
+ reason: `There is no ${found.relativePath} in the project to start: export the build first, with project_export run.`
170
+ };
171
+ }
172
+ if (!isWithinRoot(realAsFarAsItExists(projectPath), real)) {
173
+ return {
174
+ ok: false,
175
+ reason: `${found.relativePath} leads outside the project, to ${real}, so it is not started.`
176
+ };
177
+ }
178
+ if (!statSync(real).isFile()) {
179
+ return { ok: false, reason: `${found.relativePath} is not a file, so there is nothing to start.` };
180
+ }
181
+ return { ok: true, absolutePath: real, relativePath: found.relativePath };
182
+ }
183
+ var RESOURCE_SCHEME = "res://", OTHER_SCHEME, KEPT_ID;
184
+ var init_paths = __esm(() => {
185
+ OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
186
+ KEPT_ID = /^[A-Za-z0-9_]+$/;
187
+ });
188
+
71
189
  // src/spawn.ts
72
190
  import {
73
191
  execFile,
@@ -243,7 +361,7 @@ var init_windows_ask = __esm(() => {
243
361
 
244
362
  // src/process-children.ts
245
363
  import { readFileSync } from "node:fs";
246
- import { basename } from "node:path";
364
+ import { basename as basename2 } from "node:path";
247
365
  async function startTimesOf(pids) {
248
366
  const began = new Map;
249
367
  if (pids.length === 0) {
@@ -345,7 +463,7 @@ function listingFailure(error) {
345
463
  const failed = error;
346
464
  const how = failed.killed === true ? `was killed after the ${ASK_TIMEOUT_MS}ms budget${failed.signal ? ` (${failed.signal})` : ""}` : failed.signal ? `was ended by ${failed.signal}` : failed.code !== undefined && failed.code !== null ? `exited with ${failed.code}` : "failed";
347
465
  const said = typeof failed.stderr === "string" ? failed.stderr.trim() : "";
348
- const asked = basename((failed.cmd ?? "").split(" ")[0] ?? "") || "the listing";
466
+ const asked = basename2((failed.cmd ?? "").split(" ")[0] ?? "") || "the listing";
349
467
  return `${asked} ${how}${said === "" ? ", with nothing on stderr" : `; stderr: ${said.slice(0, 400)}`}`;
350
468
  }
351
469
  function whyTheProcessTreeFailed() {
@@ -440,11 +558,21 @@ function ancestorsIn(tree, pid, root) {
440
558
  }
441
559
  return chain;
442
560
  }
561
+ function editorsOfProjectIn(tree, projectPath) {
562
+ const found = [];
563
+ for (const [pid, listed] of tree) {
564
+ const read = readCommandLine(listed.command);
565
+ if (read.editor && read.oneShot === null && read.projectPath !== null && /godot/i.test(read.executable) && !CONSOLE_WRAPPER.test(read.executable) && isSameDirectory(read.projectPath, projectPath)) {
566
+ found.push(pid);
567
+ }
568
+ }
569
+ return found.sort((a, b) => a - b);
570
+ }
443
571
  function readCommandLine(command) {
444
572
  const all = commandWords(command);
445
573
  const separator = all.findIndex((word) => word === "--" || word === "++");
446
574
  const words = separator === -1 ? all : all.slice(0, separator);
447
- const executable = words[0] === undefined ? "" : basename(words[0].replaceAll("\\", "/"));
575
+ const executable = words[0] === undefined ? "" : basename2(words[0].replaceAll("\\", "/"));
448
576
  const at = words.indexOf("--path");
449
577
  let projectPath = null;
450
578
  if (at !== -1 && at + 1 < words.length) {
@@ -507,8 +635,9 @@ async function askPosix() {
507
635
  });
508
636
  return stdout;
509
637
  }
510
- var ticksAnswered, ASK_TIMEOUT_MS = 15000, lastListingFailure, ONE_SHOT_OPTIONS;
638
+ var ticksAnswered, ASK_TIMEOUT_MS = 15000, lastListingFailure, ONE_SHOT_OPTIONS, CONSOLE_WRAPPER;
511
639
  var init_process_children = __esm(() => {
640
+ init_paths();
512
641
  init_spawn();
513
642
  init_windows_ask();
514
643
  ONE_SHOT_OPTIONS = [
@@ -532,13 +661,14 @@ var init_process_children = __esm(() => {
532
661
  "--convert-3to4",
533
662
  "--validate-conversion-3to4"
534
663
  ];
664
+ CONSOLE_WRAPPER = /_console(\.exe)?$/i;
535
665
  });
536
666
 
537
667
  // src/bridge-announce.ts
538
668
  import { mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
539
- import { dirname, join } from "node:path";
669
+ import { dirname as dirname2, join as join2 } from "node:path";
540
670
  function announcementPath(projectPath) {
541
- return join(projectPath, ".godot", "gdharness-bridge.json");
671
+ return join2(projectPath, ".godot", "gdharness-bridge.json");
542
672
  }
543
673
  function readAnnouncement(path) {
544
674
  try {
@@ -585,7 +715,7 @@ function announcementIsOurs(path) {
585
715
  function announceBridge(projectPath, bridge) {
586
716
  const path = announcementPath(projectPath);
587
717
  try {
588
- mkdirSync(dirname(path), { recursive: true });
718
+ mkdirSync(dirname2(path), { recursive: true });
589
719
  const announcement = {
590
720
  protocol: BRIDGE_ANNOUNCE_PROTOCOL,
591
721
  host: bridge.host,
@@ -652,15 +782,15 @@ import {
652
782
  openSync,
653
783
  readFileSync as readFileSync3,
654
784
  rmSync as rmSync2,
655
- statSync,
785
+ statSync as statSync2,
656
786
  writeFileSync as writeFileSync2,
657
787
  writeSync
658
788
  } from "node:fs";
659
789
  import { tmpdir as tmpdir2 } from "node:os";
660
- import { join as join2 } from "node:path";
790
+ import { join as join3 } from "node:path";
661
791
  import { setTimeout as delay } from "node:timers/promises";
662
792
  function helperAssembly(directory = tmpdir2()) {
663
- return join2(directory, `gdharness-desktop-${DIGEST}.dll`);
793
+ return join3(directory, `gdharness-desktop-${DIGEST}.dll`);
664
794
  }
665
795
  async function ensureHelperCompiled(directory = tmpdir2(), budgetMs = HELPER_COMPILE_MS, wait = true) {
666
796
  const assembly = helperAssembly(directory);
@@ -686,7 +816,7 @@ async function ensureHelperCompiled(directory = tmpdir2(), budgetMs = HELPER_COM
686
816
  }
687
817
  try {
688
818
  const holder = Number(readFileSync3(lock, "utf8").trim());
689
- const old = Date.now() - statSync(lock).mtimeMs > budgetMs;
819
+ const old = Date.now() - statSync2(lock).mtimeMs > budgetMs;
690
820
  if (old || Number.isInteger(holder) && holder > 0 && !alive(holder)) {
691
821
  rmSync2(lock, { force: true });
692
822
  continue;
@@ -700,8 +830,8 @@ async function ensureHelperCompiled(directory = tmpdir2(), budgetMs = HELPER_COM
700
830
  return `the desktop helper was not compiled within ${String(budgetMs)} ms`;
701
831
  }
702
832
  async function compileHelper(directory, assembly, budgetMs) {
703
- const scratch = mkdtempSync(join2(directory, "gdharness-desktop-source-"));
704
- const source = join2(scratch, "helper.cs");
833
+ const scratch = mkdtempSync(join3(directory, "gdharness-desktop-source-"));
834
+ const source = join3(scratch, "helper.cs");
705
835
  writeFileSync2(source, SOURCE, "utf8");
706
836
  const literal = (path) => `'${path.replaceAll("'", "''")}'`;
707
837
  const script = [
@@ -8135,124 +8265,6 @@ var init_config_formats = __esm(() => {
8135
8265
  init_dist();
8136
8266
  });
8137
8267
 
8138
- // src/paths.ts
8139
- import { realpathSync, statSync as statSync2 } from "node:fs";
8140
- import { basename as basename2, dirname as dirname2, isAbsolute, join as join3, relative, resolve, sep } from "node:path";
8141
- function realAsFarAsItExists(path) {
8142
- const missing = [];
8143
- let at = path;
8144
- for (;; ) {
8145
- try {
8146
- return join3(realpathSync.native(at), ...missing);
8147
- } catch {
8148
- const parent = dirname2(at);
8149
- if (parent === at) {
8150
- return path;
8151
- }
8152
- missing.unshift(basename2(at));
8153
- at = parent;
8154
- }
8155
- }
8156
- }
8157
- function isSameDirectory(onePath, otherPath) {
8158
- return relative(realAsFarAsItExists(resolve(onePath)), realAsFarAsItExists(resolve(otherPath))) === "";
8159
- }
8160
- function isWithinRoot(rootPath, candidatePath) {
8161
- const step = relative(resolve(rootPath), resolve(candidatePath));
8162
- return step !== ".." && !step.startsWith(`..${sep}`) && !isAbsolute(step);
8163
- }
8164
- function keptInside(given) {
8165
- const at = given.indexOf("::");
8166
- const inner = at === -1 ? "" : given.slice(at + 2);
8167
- return KEPT_ID.test(inner) ? { file: given.slice(0, at), inner } : { file: given, inner: undefined };
8168
- }
8169
- function resolveWithinProject(projectPath, candidatePath, options = {}) {
8170
- const trimmed = candidatePath.trim();
8171
- if (trimmed === "") {
8172
- return { ok: false, reason: "Path is empty." };
8173
- }
8174
- if (trimmed.includes("\x00")) {
8175
- return { ok: false, reason: "Path contains a null byte." };
8176
- }
8177
- const root = resolve(projectPath);
8178
- const withoutScheme = trimmed.startsWith(RESOURCE_SCHEME) ? trimmed.slice(RESOURCE_SCHEME.length) : isAbsolute(trimmed) && isWithinRoot(root, trimmed) ? relative(root, resolve(trimmed)).split(sep).join("/") : trimmed;
8179
- if (withoutScheme.startsWith("uid://")) {
8180
- return {
8181
- ok: false,
8182
- reason: `Path '${trimmed}' is a UID, and a file is judged here by where it is on disk, which a UID does not say. Give the file's res:// path.`
8183
- };
8184
- }
8185
- const scheme = OTHER_SCHEME.exec(withoutScheme);
8186
- if (scheme) {
8187
- const elsewhere = scheme[0].toLowerCase() === "user://" ? " A game's user:// is a directory of its own outside the project: a running game's is under userDir in editor_status runtimes and in editor_run start's runtime, where a file reader of your own reads it." : "";
8188
- return {
8189
- ok: false,
8190
- reason: `Path '${trimmed}' uses the '${scheme[0]}' scheme, which names somewhere other than this project.${elsewhere}`
8191
- };
8192
- }
8193
- if (withoutScheme.startsWith("/") || withoutScheme.startsWith("\\") || isAbsolute(withoutScheme)) {
8194
- const tail = withoutScheme.split(/[\\/]+/).filter((step) => step !== "" && !step.endsWith(":"));
8195
- const besideIt = isAbsolute(trimmed) && isWithinRoot(realAsFarAsItExists(dirname2(resolve(trimmed))), realAsFarAsItExists(root));
8196
- const example = tail.slice(besideIt ? -1 : -2).join("/") || "scenes/main.tscn";
8197
- return {
8198
- ok: false,
8199
- reason: `Path '${trimmed}' is absolute and outside the project; give a path inside it, such as '${example}'.`
8200
- };
8201
- }
8202
- const kept = keptInside(trimmed);
8203
- if (kept.inner !== undefined) {
8204
- return {
8205
- ok: false,
8206
- reason: `Path '${trimmed}' names the resource ${kept.inner} kept inside ${kept.file}, not a file. resource_edit read reads one by that path; this argument takes a file.`
8207
- };
8208
- }
8209
- if (withoutScheme.includes(":")) {
8210
- return {
8211
- ok: false,
8212
- reason: `Path '${trimmed}' contains ':', which no file inside a project is named with.`
8213
- };
8214
- }
8215
- const absolutePath = resolve(root, withoutScheme);
8216
- if (!isWithinRoot(root, absolutePath)) {
8217
- return { ok: false, reason: `Path '${trimmed}' resolves outside the project directory.` };
8218
- }
8219
- const relativePath = relative(root, absolutePath).split(sep).join("/");
8220
- if (relativePath === "" && options.root !== true) {
8221
- return { ok: false, reason: `Path '${trimmed}' is the project directory itself, not a file in it.` };
8222
- }
8223
- return { ok: true, absolutePath, relativePath };
8224
- }
8225
- function exportedBuild(projectPath, candidatePath) {
8226
- const found = resolveWithinProject(projectPath, candidatePath);
8227
- if (!found.ok) {
8228
- return found;
8229
- }
8230
- let real;
8231
- try {
8232
- real = realpathSync.native(found.absolutePath);
8233
- } catch {
8234
- return {
8235
- ok: false,
8236
- reason: `There is no ${found.relativePath} in the project to start: export the build first, with project_export run.`
8237
- };
8238
- }
8239
- if (!isWithinRoot(realAsFarAsItExists(projectPath), real)) {
8240
- return {
8241
- ok: false,
8242
- reason: `${found.relativePath} leads outside the project, to ${real}, so it is not started.`
8243
- };
8244
- }
8245
- if (!statSync2(real).isFile()) {
8246
- return { ok: false, reason: `${found.relativePath} is not a file, so there is nothing to start.` };
8247
- }
8248
- return { ok: true, absolutePath: real, relativePath: found.relativePath };
8249
- }
8250
- var RESOURCE_SCHEME = "res://", OTHER_SCHEME, KEPT_ID;
8251
- var init_paths = __esm(() => {
8252
- OTHER_SCHEME = /^[a-zA-Z][a-zA-Z\d+.-]+:\/\//;
8253
- KEPT_ID = /^[A-Za-z0-9_]+$/;
8254
- });
8255
-
8256
8268
  // src/runner.ts
8257
8269
  import { existsSync as existsSync2 } from "node:fs";
8258
8270
  import { dirname as dirname3, join as join4 } from "node:path";
@@ -9905,6 +9917,29 @@ function cachedClasses(projectPath) {
9905
9917
  }
9906
9918
  return listed;
9907
9919
  }
9920
+ function cachedEntries(projectPath) {
9921
+ const text = readWhole(join6(projectPath, ".godot", "global_script_class_cache.cfg"));
9922
+ if (text === null) {
9923
+ return null;
9924
+ }
9925
+ const listed = new Map;
9926
+ for (const entry of text.matchAll(/\{[^{}]*\}/g)) {
9927
+ const named = /"class":\s*&"([^"]+)"/.exec(entry[0])?.[1];
9928
+ if (named !== undefined) {
9929
+ listed.set(named, entry[0]);
9930
+ }
9931
+ }
9932
+ return listed;
9933
+ }
9934
+ function cacheChangesBetween(before, after) {
9935
+ if (before === null || after === null) {
9936
+ return {};
9937
+ }
9938
+ const added = [...after.keys()].filter((name) => !before.has(name)).sort();
9939
+ const removed = [...before.keys()].filter((name) => !after.has(name)).sort();
9940
+ const changed = [...after.keys()].filter((name) => before.has(name) && before.get(name) !== after.get(name)).sort();
9941
+ return { added, removed, changed };
9942
+ }
9908
9943
  function addonOf(path) {
9909
9944
  return /^res:\/\/addons\/([^/]+)\//.exec(path)?.[1];
9910
9945
  }
@@ -9921,6 +9956,20 @@ function withAddonPathsCounted(paths) {
9921
9956
  }
9922
9957
  return { own, inAddons };
9923
9958
  }
9959
+ function classesWithAddonsCounted(classes, listed) {
9960
+ const kept = [];
9961
+ const inAddons = {};
9962
+ for (const entry of classes) {
9963
+ const path = entry !== null && typeof entry === "object" ? entry["path"] : "";
9964
+ const addon = addonOf(typeof path === "string" ? path : "");
9965
+ if (addon === undefined || listed.has(addon)) {
9966
+ kept.push(entry);
9967
+ } else {
9968
+ inAddons[addon] = (inAddons[addon] ?? 0) + 1;
9969
+ }
9970
+ }
9971
+ return { classes: kept, inAddons };
9972
+ }
9924
9973
  function withAddonClassesCounted(rebuilt, listed) {
9925
9974
  const counted = { ...rebuilt };
9926
9975
  for (const key of ["added", "changed"]) {
@@ -14231,7 +14280,8 @@ async function operationOutcome(engine, operation, params, projectPath) {
14231
14280
  ...afterAnswered
14232
14281
  };
14233
14282
  }
14234
- const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). Anything it reported on the way out is under ${answered === -1 ? "engine_messages" : "engine_messages_after_answer"}.`;
14283
+ const onTheWayOut = answered === -1 ? problems : after;
14284
+ const afterAnswer = ran.exitCode === 0 && ran.failure === null ? undefined : `The operation finished and wrote this answer, and then the engine did not exit cleanly (${howItEnded(ran)}). ${onTheWayOut.length === 0 ? "It printed no error or warning on the way out." : `What it reported on the way out is under ${answered === -1 ? "engine_messages" : "engine_messages_after_answer"}.`}`;
14235
14285
  return {
14236
14286
  ok: true,
14237
14287
  payload: answer.payload,
@@ -14794,7 +14844,7 @@ var init_ports = __esm(() => {
14794
14844
  import { existsSync as existsSync16, realpathSync as realpathSync2 } from "node:fs";
14795
14845
  import { readFile, realpath } from "node:fs/promises";
14796
14846
  import { createConnection } from "node:net";
14797
- import { dirname as dirname8, join as join20, resolve as resolve6 } from "node:path";
14847
+ import { dirname as dirname8, extname as extname2, join as join20, resolve as resolve6 } from "node:path";
14798
14848
  import { setTimeout as delay4 } from "node:timers/promises";
14799
14849
  import { fileURLToPath, pathToFileURL } from "node:url";
14800
14850
  function initializeTimeoutMs() {
@@ -15485,6 +15535,11 @@ async function resolveLSPPaths(projectPathValue, scriptPathValue) {
15485
15535
  } catch {
15486
15536
  throw new ArgumentRefusal(`Script file does not exist: res://${contained.relativePath}`);
15487
15537
  }
15538
+ if (!scriptPath.toLowerCase().endsWith(".gd")) {
15539
+ const said = `res://${contained.relativePath} is not a GDScript file, and the language server reads .gd files only`;
15540
+ const kind = extname2(scriptPath).toLowerCase();
15541
+ throw new ArgumentRefusal(kind === ".tscn" || kind === ".scn" ? `${said}: scene_tree reads a scene's nodes, and with properties the script each carries.` : kind === ".tres" || kind === ".res" ? `${said}: resource_edit read reads a resource.` : `${said}.`);
15542
+ }
15488
15543
  if (!isWithinRoot(projectPath, scriptPath)) {
15489
15544
  throw new ArgumentRefusal("scriptPath resolves outside the project root boundary.");
15490
15545
  }
@@ -15633,7 +15688,7 @@ var init_lsp_client = __esm(() => {
15633
15688
 
15634
15689
  // src/resources.ts
15635
15690
  import { readFileSync as readFileSync17, realpathSync as realpathSync3 } from "node:fs";
15636
- import { extname as extname2, resolve as resolve8 } from "node:path";
15691
+ import { extname as extname3, resolve as resolve8 } from "node:path";
15637
15692
  function ensureProjectPath(getProjectPath) {
15638
15693
  const projectPath = getProjectPath();
15639
15694
  if (!projectPath) {
@@ -15692,7 +15747,7 @@ function parseGodotUri(uri) {
15692
15747
  }
15693
15748
  function ensureAllowedExtension(kind, filePath) {
15694
15749
  const allowed = EXTENSIONS[kind];
15695
- if (!allowed.includes(extname2(filePath).toLowerCase())) {
15750
+ if (!allowed.includes(extname3(filePath).toLowerCase())) {
15696
15751
  throw new Refusal(`godot://${kind}/ reads only ${allowed.join(", ")} files.`);
15697
15752
  }
15698
15753
  }
@@ -16040,6 +16095,25 @@ function shippedRuntimeDigest() {
16040
16095
  function installedEditorDigest(projectPath) {
16041
16096
  return digestMarker(projectPath, ADDONS[0]);
16042
16097
  }
16098
+ function editorUpgradeNote(sameCode, editors) {
16099
+ if (sameCode) {
16100
+ return ` 1. Nothing for the editor: this upgrade left the editor addons the same code as before,
16101
+ ` + ` so it gives an open editor nothing to restart for. editor_status says addonIsStale
16102
+ ` + " if the editor is running other code.";
16103
+ }
16104
+ if (editors === null) {
16105
+ return ` 1. An editor of this project that is open is still running the addons it loaded at
16106
+ ` + ` startup. Restart it with the editor_launch restart tool, which closes and reopens
16107
+ ` + " the window.";
16108
+ }
16109
+ if (editors.length === 0) {
16110
+ return ` 1. Nothing for the editor: none of this project is running, and the next one opened loads
16111
+ these addons.`;
16112
+ }
16113
+ const which = editors.length === 1 ? `The open editor (pid ${editors[0]}) is` : `The open editors (pids ${editors.join(", ")}) are`;
16114
+ return ` 1. ${which} still running the addons loaded at startup. Restart with the
16115
+ ` + " editor_launch restart tool, which closes and reopens the window.";
16116
+ }
16043
16117
  function installedRuntimeDigest(projectPath) {
16044
16118
  return digestMarker(projectPath, RUNTIME_ADDON);
16045
16119
  }
@@ -16511,6 +16585,11 @@ var init_tool_definitions = __esm(() => {
16511
16585
  },
16512
16586
  nodePath: { type: "string", description: "Target node, relative to the AnimationPlayer root." },
16513
16587
  property: { type: "string", description: "For property tracks: the property to animate." },
16588
+ update: {
16589
+ type: "string",
16590
+ enum: ["continuous", "discrete", "capture"],
16591
+ description: "For property tracks: how the value moves between keys. continuous blends toward the next key, so a bool or an int changes halfway to it; discrete holds each key's value until the next; capture blends from the value the property holds when playing starts. Without it a new track is made as the editor makes one: as the RESET animation's track for the same property, otherwise continuous for a float, vector, colour or transform and discrete for anything else. The answer gives the track's update."
16592
+ },
16514
16593
  method: {
16515
16594
  type: "string",
16516
16595
  description: "For method tracks: the method to call. A method track holds every method key for its node, so a key calling another method joins the node's track, with existingTrack true. The answer's methodFound says whether the target node has it; a method it lacks is still added, while a nodePath that reaches no node is refused, since it may be written next, and until it exists each call the track makes fails. A key whose args the method cannot take (too few or too many, or an object where it takes a value or the reverse) is written too, and answers argumentsFit false with misfit saying why. remove_track: only the keys calling this method."
@@ -16651,7 +16730,12 @@ var init_tool_definitions = __esm(() => {
16651
16730
  include: {
16652
16731
  type: "array",
16653
16732
  items: { type: "string", enum: [...PROJECT_INFO_SECTIONS] },
16654
- description: "Extra sections: registered autoloads, the classes the project's scripts declare with class_name, each with its script, read from the files, the input actions project.godot holds with their events (an engine ui_ action only when the project rebinds it, as engineAction), addons and whether each is enabled, export presets (runnable marks the one preset of its platform the editor's one-click deploy uses), the audio bus layout, a health report, or export validation. health and validation compile every script of the project in headless engines and name each that fails with its first error, each file a scene, resource or script refers to that does not exist, each input action a script reads by a name written in the call that neither the project nor the engine defines, with every line reading it, and each signal connection in a scene whose method or signal the node at its end does not have, judged from the node's type and scripts as their files are written."
16733
+ description: "Extra sections: registered autoloads, the classes the project's scripts declare with class_name, each with its script, read from the files (those an addon declares counted per addon under inAddons, unless addonClasses names the addon), the input actions project.godot holds with their events (an engine ui_ action only when the project rebinds it, as engineAction), addons and whether each is enabled, export presets (runnable marks the one preset of its platform the editor's one-click deploy uses), the audio bus layout, a health report, or export validation. health and validation compile every script of the project in headless engines and name each that fails with its first error, each file a scene, resource or script refers to that does not exist, each input action a script reads by a name written in the call that neither the project nor the engine defines, with every line reading it, and each signal connection in a scene whose method or signal the node at its end does not have, judged from the node's type and scripts as their files are written."
16734
+ },
16735
+ addonClasses: {
16736
+ type: "array",
16737
+ items: { type: "string" },
16738
+ description: "For classes: the addons, by their folder under res://addons, whose classes are listed by name and script like the project's own, rather than counted under inAddons. Default none: a game with two large addons answered 686 classes, 19 of them its own."
16655
16739
  },
16656
16740
  preset: { type: "string", description: "For validation: the export preset to validate against." },
16657
16741
  detail: {
@@ -17144,7 +17228,7 @@ var init_tool_definitions = __esm(() => {
17144
17228
  name: "scene_node",
17145
17229
  title: "Scene nodes",
17146
17230
  description: `One node in a scene file: add, read, set, duplicate, reparent or delete it, paint TileMap cells, or bake a navigation region. 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, and answers nodePath as its new path. A rename or a reparent carries the node paths naming the node, as the editor's rename does: NodePath values on nodes (in arrays, dictionaries, built-in resources, %unique paths) and the tracks its players' animations hold, saving a library kept in a .tres of its own, and answers each rewrite under retargeted and each path it left under notRetargeted. The move is followed into every other .tscn that instances or inherits this scene at any depth: the editable children, values, added nodes and connections named by the old path are rewritten, since Godot finds a moved node's values by unique_id but editable children only by path and drops the values kept below them on the next save, and that scene's node paths and tracks are carried as above. A delete rewrites no path, and answers under stillNaming what named the deleted node and now names nothing. 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. A node this scene inherits takes values of this scene's own, and so does a node inside an instance of another scene once that instance's editableChildren is true; neither can be moved, renamed, copied or deleted, since the other scene makes it again on load. ${WITHOUT_AN_EDITOR}`,
17147
- answer: "get answers nodeType, the node's engine class, with scriptClass when its script declares a class_name; nodePath, its path from the root however it was named; setHere, the properties this scene's own file sets on it; and valuesFrom, the scene every other value comes from, when another scene makes the node. A set that gives a property the value the node has without it (what the scene it comes from gives, or its default) leaves it out of the file, as the editor does, and answers it under notStored, with notStoredNote saying what it follows now. retargeted lists each path a rename or a reparent rewrote, as {scenePath, nodePath, property or animation and track, from, to}, and notRetargeted each it left, with a reason: a track kept in another scene or an imported file, or a value another scene gives that the move left naming nothing. followed names the other scenes a move rewrote, and notFollowed any left as they were, with why. scriptsNaming lists each line of a script that reads a node a rename, a reparent or a delete took by the path it had, as {scenePath, nodePath, script, line, from, to}: the script is on the node at nodePath, or is one that node's script extends, in this scene or one instancing or inheriting it, and to is the path that reaches the node from there now, absent after a delete. It reads $Path, %Unique, and a string handed to get_node, get_node_or_null or has_node on the node itself; a path built at run time is not seen. Scripts are left as they were, as the editor's own rename leaves them, so each line listed fails until it is changed. A 2D bake_navigation that read no obstacle says in its note that all its outlines enclose is walkable, and names the nodes giving obstacles that sit under no region. stillNaming lists each node path and track, here and in the scenes instancing or inheriting this one, that named a deleted node, and deletedWithIt each node of this scene's own that went with it, the nodes inside an instance going with the instance. groups lists, sorted, every group a node is in, on a get, an add that gave addGroups and every set that changes them; get adds groupsHere, the groups this file puts it in, when another scene puts it in others, and layers, each layer mask it answers (collision_layer, collision_mask, visibility_layer) as the layers it holds, {layer, name} with the name project.godot gives the layer. Each of these lists is answered only when it names anything. add and set answer each property given under properties as the node holds it once written; alsoChanged gives the other properties the engine changed in step with a set, as a Control's offsets with its size, and those a reparent changed to keep the node where it is drawn, as {was, now}; namesNoNode, each node path written naming no node; sharedWith, for a property written through a resource built into the scene, each other {node, property} holding it, which changed too; setByScript, what a script that is not a tool set as a headless write ran, which the file does not hold; loadsAs, a value given that the file holds and the node's own script changes as the scene loads, as it loads. get adds placement, a Node3D's position, rotation_degrees and scale from its transform. " + ALSO_SAVED,
17231
+ answer: "get answers nodeType, the node's engine class, with scriptClass when its script declares a class_name; nodePath, its path from the root however it was named; setHere, the properties this scene's own file sets on it; and valuesFrom, the scene every other value comes from, when another scene makes the node. A set that gives a property the value the node has without it (what the scene it comes from gives, or its default) leaves it out of the file, as the editor does, and answers it under notStored, with notStoredNote saying what it follows now. retargeted lists each path a rename or a reparent rewrote, as {scenePath, nodePath, property or animation and track, from, to}, and notRetargeted each it left, with a reason: a track kept in another scene or an imported file, or a value another scene gives that the move left naming nothing. followed names the other scenes a move rewrote, and notFollowed any left as they were, with why. scriptsNaming lists each line of a script that reads a node a rename, a reparent or a delete took by the path it had, as {scenePath, nodePath, script, line, from, to}: the script is on the node at nodePath, or is one that node's script extends, in this scene or one instancing or inheriting it, and to is the path that reaches the node from there now, absent after a delete. It reads $Path, %Unique, and a string handed to get_node, get_node_or_null or has_node on the node itself; a path built at run time is not seen. Scripts are left as they were, as the editor's own rename leaves them, so each line listed fails until it is changed. A 2D bake_navigation that read no obstacle says in its note that all its outlines enclose is walkable, and names the nodes giving obstacles that sit under no region. stillNaming lists each node path and track, here and in the scenes instancing or inheriting this one, that named a deleted node; a property typed as a node, or an array or dictionary of nodes, holds the node itself, so the one that held a deleted node holds null there, as the editor saves it, and its entry adds cleared: true. deletedWithIt lists each node of this scene's own that went with it, the nodes inside an instance going with the instance. groups lists, sorted, every group a node is in, on a get, an add that gave addGroups and every set that changes them; get adds groupsHere, the groups this file puts it in, when another scene puts it in others, and layers, each layer mask it answers (collision_layer, collision_mask, visibility_layer) as the layers it holds, {layer, name} with the name project.godot gives the layer. Each of these lists is answered only when it names anything. add and set answer each property given under properties as the node holds it once written; alsoChanged gives the other properties the engine changed in step with a set, as a Control's offsets with its size, and those a reparent changed to keep the node where it is drawn or that the new parent sets, as a container gives a Control its layout_mode, as {was, now}; namesNoNode, each node path written naming no node; sharedWith, for a property written through a resource built into the scene, each other {node, property} holding it, which changed too; setByScript, what a script that is not a tool set as a headless write ran, which the file does not hold; loadsAs, a value given that the file holds and the node's own script changes as the scene loads, as it loads. get adds placement, a Node3D's position, rotation_degrees and scale from its transform. " + ALSO_SAVED,
17148
17232
  parameters: {
17149
17233
  projectPath: PROJECT_PATH,
17150
17234
  scenePath: SCENE_PATH,
@@ -17186,7 +17270,11 @@ var init_tool_definitions = __esm(() => {
17186
17270
  ops: ["set"],
17187
17271
  description: "set: groups to take the node out of. One it is not in is refused, and so is one another scene puts it in (the scene that makes it, instances it or is inherited here), which only that scene can take it out of."
17188
17272
  },
17189
- newName: { type: "string", ops: ["duplicate"], description: "duplicate: the copy's name." },
17273
+ newName: {
17274
+ type: "string",
17275
+ ops: ["duplicate"],
17276
+ description: "duplicate: the copy's name. The copy has the nodes under the original and their connections, as the editor's Duplicate makes it: each connection keeps its flags, binds and unbinds, and one between two copied nodes joins the copies."
17277
+ },
17190
17278
  newParentPath: {
17191
17279
  type: "string",
17192
17280
  ops: ["reparent"],
@@ -17258,7 +17346,7 @@ var init_tool_definitions = __esm(() => {
17258
17346
  {
17259
17347
  name: "scene_signal",
17260
17348
  title: "Scene signals",
17261
- 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 connection carries binds, the values passed after the signal's own, and unbinds, how many of the signal's own are dropped first, as the editor's connect dialog offers them; each is answered only when there is one. A connect of a pair already connected says alreadyConnected, with flagsBefore, bindsBefore and unbindsBefore when it changed them, and keeps the binds and unbinds it had unless the call gives new ones. A connect to a method the target does not have is still written, as the editor's own dialog allows, since the method may be written next; methodFound false says so, and until the method exists the signal fails each time it is emitted. One whose method cannot take what the signal passes, with its binds and unbinds (too few or too many arguments, an object where the method takes a value or the reverse, or an engine class unrelated to the one it takes), is written too, and answers argumentsFit false with misfit saying why; list gives each connection its methodFound, and misfit when it has one. list marks editable false on a connection a scene this one instances or inherits makes, on its own root as well as inside it, and names that scene under madeIn; only that scene can change it, so a disconnect of one is refused. ${WITHOUT_AN_EDITOR} ${ALSO_SAVED}`,
17349
+ 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 connection carries binds, the values passed after the signal's own, and unbinds, how many of the signal's own are dropped first, as the editor's connect dialog offers them; each is answered only when there is one. A connect of a pair already connected says alreadyConnected, with flagsBefore, bindsBefore and unbindsBefore when it changed them, and keeps the binds and unbinds it had unless the call gives new ones. A connect to a method the target does not have is still written, as the editor's own dialog allows, since the method may be written next; methodFound false says so, and until the method exists the signal fails each time it is emitted. That holds only without binds and unbinds: the engine makes a connection that binds or unbinds arguments only to a method its target has, and a scene loads without one aimed elsewhere, so such a connect is refused. A file can hold one already, written by hand or before its method was renamed: the engine loads the scene without it and a save drops it, so every tool that writes a .tscn puts it back as the file held it and says so under unmadeConnections, a write to a binary .scn holding one is refused, and a disconnect takes it out. One whose method cannot take what the signal passes, with its binds and unbinds (too few or too many arguments, an object where the method takes a value or the reverse, or an engine class unrelated to the one it takes), is written too, and answers argumentsFit false with misfit saying why; list gives each connection its methodFound, and misfit when it has one. list marks editable false on a connection a scene this one instances or inherits makes, on its own root as well as inside it, and names that scene under madeIn; only that scene can change it, so a disconnect of one is refused. ${WITHOUT_AN_EDITOR} ${ALSO_SAVED}`,
17262
17350
  parameters: {
17263
17351
  projectPath: PROJECT_PATH,
17264
17352
  scenePath: SCENE_PATH,
@@ -18065,7 +18153,7 @@ var init_tool_definitions = __esm(() => {
18065
18153
  property: {
18066
18154
  type: "string",
18067
18155
  ops: ["property", "find"],
18068
- description: `property: which one to read. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A key holding a colon, a bracket or a quote is written in double quotes, 'stock:"base:dew"', as a refusal listing the keys writes it. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A vector, a colour, a rectangle or a transform is stepped into by its components as GDScript names them, so one axis is one number: "size:x", "global_position:y", "modulate:a", "global_transform:origin:z"; a name the value does not have is refused, naming the ones it has. It also answers the calls that read it with no arguments, by its type, such as "linear_velocity:length()" or "global_transform:get_origin()"; one it does not answer is refused, naming those it does. A string answers such calls too, "text:length()" or "text:strip_edges()", and "text:to_utf32_buffer():to_int32_array()" gives its code points, which tell a non-breaking space (160) from the space an answer shows in its place. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. An engine method that reads takes literal arguments in its brackets, words in double quotes, numbers, true, false or null, so a control's theme is read where it is drawn: 'get_theme_stylebox("panel"):content_margin_top' or 'get_theme_constant("separation")'. A game method is given none in a path, since nothing marks one as only reading; runtime_invoke call takes those. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.`
18156
+ description: `property: which one to read, or a constant the node's script or a script it extends declares. find: read this one off every node matched, so a panel of labels is one call rather than one per label. Colons read through what a node holds, "_game:clock:speed", which is where a game keeps what is worth asking about. A number or a key steps into a list or a map, "_game:run:roster:0:traits", which is how the lists a game keeps its state in are walked: a roster, a board, an in-tray. A key holding a colon, a bracket or a quote is written in double quotes, 'stock:"base:dew"', as a refusal listing the keys writes it. A packed list, such as a PackedStringArray, steps the same way. A negative number counts from the end. A vector, a colour, a rectangle or a transform is stepped into by its components as GDScript names them, so one axis is one number: "size:x", "global_position:y", "modulate:a", "global_transform:origin:z"; a name the value does not have is refused, naming the ones it has. It also answers the calls that read it with no arguments, by its type, such as "linear_velocity:length()" or "global_transform:get_origin()"; one it does not answer is refused, naming those it does. A string answers such calls too, "text:length()" or "text:strip_edges()", and "text:to_utf32_buffer():to_int32_array()" gives its code points, which tell a non-breaking space (160) from the space an answer shows in its place. A step written as a call, "get_viewport():gui_get_focus_owner()", calls a method that only reads (an engine method the engine marks as reading, or a game method not declared -> void, so queue_free() is refused) and walks into what it returned, which is how a question only a method answers is read in one call: which control holds the focus in the viewport this one is in. An engine method that reads takes literal arguments in its brackets, words in double quotes, numbers, true, false or null, so a control's theme is read where it is drawn: 'get_theme_stylebox("panel"):content_margin_top' or 'get_theme_constant("separation")'. A game method is given none in a path, since nothing marks one as only reading; runtime_invoke call takes those. A list or a map answers the calls that read it and take no arguments, "_game:run:board:size()" or "_game:inbox:keys()", and not those that change it, such as clear() or sort(). A step that is not there is named, and says what was there instead.`
18069
18157
  },
18070
18158
  depth: {
18071
18159
  type: "integer",
@@ -41762,6 +41850,30 @@ function cutTestRunNote(run) {
41762
41850
  }
41763
41851
  var init_cut_test_run = () => {};
41764
41852
 
41853
+ // src/godot-variant.ts
41854
+ var FLAG_64, MOST_MESSAGE_BYTES;
41855
+ var init_godot_variant = __esm(() => {
41856
+ FLAG_64 = 1 << 16;
41857
+ MOST_MESSAGE_BYTES = (8 << 20) + 4;
41858
+ });
41859
+
41860
+ // src/game-debugger.ts
41861
+ var PAUSE_MS = 30000, STEP_MS = 1e4, RUNTIME_ADDON2 = "res://addons/gdharness_runtime/", RUNTIME_AUTOLOAD2, READ_MS = 5000, EVALUATE_MS = 60000, REPLIES;
41862
+ var init_game_debugger = __esm(() => {
41863
+ init_godot_variant();
41864
+ init_setup();
41865
+ RUNTIME_AUTOLOAD2 = `${RUNTIME_ADDON2}runtime_autoload.gd`;
41866
+ REPLIES = new Set([
41867
+ "debug_enter",
41868
+ "debug_exit",
41869
+ "stack_dump",
41870
+ "stack_frame_vars",
41871
+ "stack_frame_var",
41872
+ "evaluation_return",
41873
+ "scene:inspect_object"
41874
+ ]);
41875
+ });
41876
+
41765
41877
  // src/dap_client.ts
41766
41878
  import { createConnection as createConnection3 } from "node:net";
41767
41879
  import { setTimeout as delay6 } from "node:timers/promises";
@@ -41804,11 +41916,13 @@ class GodotDAPClient {
41804
41916
  onBreakpointsChanged = null;
41805
41917
  scopesPatienceMs;
41806
41918
  stepPatienceMs;
41919
+ evaluatePatienceMs;
41807
41920
  constructor(port = portFromEnv("GDHARNESS_DAP_PORT", DEFAULT_DAP_PORT), host = "127.0.0.1", patience = {}) {
41808
41921
  this.port = port;
41809
41922
  this.host = host;
41810
41923
  this.scopesPatienceMs = patience.scopesMs ?? SCOPES_TIMEOUT_MS;
41811
41924
  this.stepPatienceMs = patience.stepMs ?? STEP_LANDS_WITHIN_MS;
41925
+ this.evaluatePatienceMs = patience.evaluateMs ?? EVALUATE_MS;
41812
41926
  this.pendingRequests = new Map;
41813
41927
  }
41814
41928
  async connect() {
@@ -42312,7 +42426,7 @@ class GodotDAPClient {
42312
42426
  if (members?.variables?.some((variable) => variable["name"] === "self") !== true) {
42313
42427
  throw new ReadRefusal(`Frame ${frame} has no instance of its script to evaluate on, as a static function called on its class has none, and the engine lets the game go on rather than answer one there, so it was not asked. Evaluate in a frame below it, or read this one's locals with debug_state variables`);
42314
42428
  }
42315
- const deadline = Date.now() + this.scopesPatienceMs;
42429
+ const deadline = Date.now() + this.evaluatePatienceMs;
42316
42430
  while (Date.now() < deadline) {
42317
42431
  try {
42318
42432
  const answered = await this.sendRequest("evaluate", { expression, frameId: frame, context: "watch" }, VARIABLES_POLL_TIMEOUT_MS);
@@ -42324,7 +42438,7 @@ class GodotDAPClient {
42324
42438
  } catch {}
42325
42439
  await delay6(200);
42326
42440
  }
42327
- throw new ReadRefusal(`What ${expression} comes to did not arrive from the game within ${this.scopesPatienceMs / 1000}s. It comes from the game through the editor: ask again`);
42441
+ throw new ReadRefusal(`What ${expression} comes to did not arrive from the game within ${this.evaluatePatienceMs / 1000}s. It comes from the game through the editor, and the first expression a game evaluates loads the script of every global class in the project first, static initialisers included, so one of those may still be running: ask again`);
42328
42442
  }
42329
42443
  isConnected() {
42330
42444
  return this.connected;
@@ -42471,6 +42585,7 @@ var init_dap_client = __esm(() => {
42471
42585
  init_answer_json();
42472
42586
  init_errors();
42473
42587
  init_framing();
42588
+ init_game_debugger();
42474
42589
  init_ports();
42475
42590
  ReadRefusal = class ReadRefusal extends Refusal {
42476
42591
  };
@@ -44040,7 +44155,7 @@ import {
44040
44155
  statSync as statSync17,
44041
44156
  writeFileSync as writeFileSync19
44042
44157
  } from "node:fs";
44043
- import { basename as basename10, dirname as dirname22, extname as extname3, join as join38, relative as relative8 } from "node:path";
44158
+ import { basename as basename10, dirname as dirname22, extname as extname4, join as join38, relative as relative8 } from "node:path";
44044
44159
  function filesUnder(directory) {
44045
44160
  return readdirSync17(directory, { withFileTypes: true }).flatMap((entry) => {
44046
44161
  const path = join38(directory, entry.name);
@@ -44160,10 +44275,10 @@ function planFileMove(projectPath, absoluteFrom, absoluteTo) {
44160
44275
  if (owner !== undefined && owner !== null) {
44161
44276
  return { ok: false, reason: `${from} belongs to ${owner} and moves with it: move ${owner} instead.` };
44162
44277
  }
44163
- if (extname3(from).toLowerCase() !== extname3(to).toLowerCase()) {
44278
+ if (extname4(from).toLowerCase() !== extname4(to).toLowerCase()) {
44164
44279
  return {
44165
44280
  ok: false,
44166
- reason: `${from} would become ${to}, and the engine picks how to read a file by its extension: give newPath the extension it has, ${extname3(from) || "none"}.`
44281
+ reason: `${from} would become ${to}, and the engine picks how to read a file by its extension: give newPath the extension it has, ${extname4(from) || "none"}.`
44167
44282
  };
44168
44283
  }
44169
44284
  }
@@ -44320,30 +44435,6 @@ var init_file_move = __esm(() => {
44320
44435
  SIDECARS = [".uid", ".import"];
44321
44436
  });
44322
44437
 
44323
- // src/godot-variant.ts
44324
- var FLAG_64, MOST_MESSAGE_BYTES;
44325
- var init_godot_variant = __esm(() => {
44326
- FLAG_64 = 1 << 16;
44327
- MOST_MESSAGE_BYTES = (8 << 20) + 4;
44328
- });
44329
-
44330
- // src/game-debugger.ts
44331
- var PAUSE_MS = 30000, STEP_MS = 1e4, RUNTIME_ADDON2 = "res://addons/gdharness_runtime/", RUNTIME_AUTOLOAD2, READ_MS = 5000, REPLIES;
44332
- var init_game_debugger = __esm(() => {
44333
- init_godot_variant();
44334
- init_setup();
44335
- RUNTIME_AUTOLOAD2 = `${RUNTIME_ADDON2}runtime_autoload.gd`;
44336
- REPLIES = new Set([
44337
- "debug_enter",
44338
- "debug_exit",
44339
- "stack_dump",
44340
- "stack_frame_vars",
44341
- "stack_frame_var",
44342
- "evaluation_return",
44343
- "scene:inspect_object"
44344
- ]);
44345
- });
44346
-
44347
44438
  // node_modules/ws/lib/constants.js
44348
44439
  var require_constants = __commonJS(function(exports, module) {
44349
44440
  var BINARY_TYPES = ["nodebuffer", "arraybuffer", "fragments"];
@@ -49221,7 +49312,7 @@ import {
49221
49312
  statSync as statSync20,
49222
49313
  writeFileSync as writeFileSync22
49223
49314
  } from "node:fs";
49224
- import { basename as basename13, dirname as dirname25, extname as extname4, isAbsolute as isAbsolute8, join as join44, normalize as normalize2, relative as relative10 } from "node:path";
49315
+ import { basename as basename13, dirname as dirname25, extname as extname5, isAbsolute as isAbsolute8, join as join44, normalize as normalize2, relative as relative10 } from "node:path";
49225
49316
  import { setTimeout as delay7 } from "node:timers/promises";
49226
49317
  import { fileURLToPath as fileURLToPath5 } from "node:url";
49227
49318
  function editorRestartTimeoutMs() {
@@ -49273,7 +49364,7 @@ function writtenFileRefusal(path, rule) {
49273
49364
  if (!isAbsolute8(path)) {
49274
49365
  return `${named} ${path} is not absolute; name the whole path, so where it is written does not depend on this server's directory.`;
49275
49366
  }
49276
- if (!rule.extensions.includes(extname4(path).toLowerCase())) {
49367
+ if (!rule.extensions.includes(extname5(path).toLowerCase())) {
49277
49368
  return `${named} ${path} does not end in ${rule.extensions.join(" or ")}, which is what ${what} is.`;
49278
49369
  }
49279
49370
  const directory = realAsFarAsItExists(dirname25(path));
@@ -49305,7 +49396,7 @@ function movieRefusal(engineArgs, projects, headless) {
49305
49396
  extensions: [".avi", ".png"],
49306
49397
  projects,
49307
49398
  writes: (where) => {
49308
- if (extname4(where).toLowerCase() !== ".png") {
49399
+ if (extname5(where).toLowerCase() !== ".png") {
49309
49400
  return [where];
49310
49401
  }
49311
49402
  const base = where.slice(0, -".png".length);
@@ -49620,16 +49711,6 @@ function endedPreviousRun(ended) {
49620
49711
  function previousRunLeft(ended) {
49621
49712
  return (ended?.ending === "refused" || ended?.ending === "lingering") && ended.pid !== null ? ended.pid : undefined;
49622
49713
  }
49623
- function editorsOfProjectIn(tree, projectPath) {
49624
- const found = [];
49625
- for (const [pid, listed] of tree) {
49626
- const read = readCommandLine(listed.command);
49627
- if (read.editor && read.oneShot === null && read.projectPath !== null && /godot/i.test(read.executable) && !CONSOLE_WRAPPER.test(read.executable) && isSameDirectory(read.projectPath, projectPath)) {
49628
- found.push(pid);
49629
- }
49630
- }
49631
- return found.sort((a, b) => a - b);
49632
- }
49633
49714
  function engineNamesOf(executable) {
49634
49715
  return CONSOLE_WRAPPER.test(executable) ? [executable, executable.replace(CONSOLE_WRAPPER, "$1")] : [executable];
49635
49716
  }
@@ -52643,6 +52724,9 @@ class GodotServer {
52643
52724
  if (forValidation.length > 0 && !include.includes("validation")) {
52644
52725
  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.`);
52645
52726
  }
52727
+ if (args["addonClasses"] !== undefined && !include.includes("classes")) {
52728
+ return this.createErrorResponse('addonClasses is read only by the classes section, which this call does not include: add "classes" to include, or leave it out.');
52729
+ }
52646
52730
  const project = this.project(args);
52647
52731
  if (!project.ok) {
52648
52732
  return project.response;
@@ -52673,6 +52757,23 @@ class GodotServer {
52673
52757
  ...compiledScripts !== undefined && (section === "health" || section === "validation") ? { compiled: compiledScripts } : {}
52674
52758
  };
52675
52759
  const outcome = await this.operation(spec.operation, params, project.value.path);
52760
+ if (section === "classes" && outcome.ok && Array.isArray(outcome.payload["classes"])) {
52761
+ const declared = outcome.payload["classes"];
52762
+ const asked = readStringArray(args, "addonClasses") ?? [];
52763
+ const addons = Object.keys(classesWithAddonsCounted(declared, new Set).inAddons).sort();
52764
+ const unknownAddons = asked.filter((addon) => !addons.includes(addon));
52765
+ if (unknownAddons.length > 0) {
52766
+ return this.createErrorResponse(`addonClasses names ${unknownAddons.join(", ")}, and no folder of that name under res://addons declares a class. ${addons.length === 0 ? "No addon declares one." : `The addons declaring classes are ${addons.join(", ")}.`}`);
52767
+ }
52768
+ const counted = classesWithAddonsCounted(declared, new Set(asked));
52769
+ info[section] = {
52770
+ ...outcome.payload,
52771
+ classes: counted.classes,
52772
+ ...Object.keys(counted.inAddons).length > 0 ? { inAddons: counted.inAddons } : {},
52773
+ ...engineExtras(outcome)
52774
+ };
52775
+ continue;
52776
+ }
52676
52777
  info[section] = outcome.ok ? { ...outcome.payload, ...engineExtras(outcome) } : { error: outcome.message, ...engineExtras(outcome) };
52677
52778
  }
52678
52779
  return this.jsonTextResponse(info);
@@ -52887,7 +52988,7 @@ ${platform}`;
52887
52988
  if (pack && debug) {
52888
52989
  return this.createErrorResponse("A pack is exported as a release: the command-line exporter has no debug pack, so debug and pack cannot both be true.", ["Leave debug out for a pack", "Or leave pack out for a debug build"]);
52889
52990
  }
52890
- const extension = extname4(output.relativePath).toLowerCase();
52991
+ const extension = extname5(output.relativePath).toLowerCase();
52891
52992
  if (pack && extension !== ".pck" && extension !== ".zip") {
52892
52993
  return this.createErrorResponse(`A pack is written as a .pck or a .zip, and ${output.relativePath} is neither: the exporter picks the format by the extension.`, ["Give outputPath ending in .pck, or .zip for a zip of the resources"]);
52893
52994
  }
@@ -53998,7 +54099,7 @@ ${error.text}`;
53998
54099
  return { ...game, keeper: game.keeper };
53999
54100
  }
54000
54101
  async askTheKeptGame(keeper, request, projectPath) {
54001
- const answered = await askTheKeepersDebugger(keeper, request, KEEPER_DEBUG_MS);
54102
+ const answered = await askTheKeepersDebugger(keeper, request, request.op === "evaluate" ? EVALUATE_MS + 2 * READ_MS : KEEPER_DEBUG_MS);
54002
54103
  if ("unreached" in answered) {
54003
54104
  return this.createErrorResponse(`The debugger of the running game did not answer: ${String(answered.unreached)}.`, [
54004
54105
  "editor_run start starts the game again, with a debugger",
@@ -57522,6 +57623,7 @@ ${error.text}`;
57522
57623
  async settleRenameOnDisk(projectPath, changed, asked) {
57523
57624
  const report = {};
57524
57625
  let ok = true;
57626
+ const cacheBefore = asked.rebuildCache ? cachedEntries(projectPath) : null;
57525
57627
  if (asked.moved) {
57526
57628
  const engine = await this.engine();
57527
57629
  const sidecarsBefore = new Set(projectFiles(projectPath, [".uid", ".import"]));
@@ -57536,7 +57638,7 @@ ${error.text}`;
57536
57638
  }
57537
57639
  if (asked.rebuildCache) {
57538
57640
  const rebuilt = await this.rebuildClassCache(projectPath);
57539
- report["classCache"] = rebuilt.ok ? rebuilt.payload : { rebuilt: false, problem: rebuilt.message };
57641
+ report["classCache"] = rebuilt.ok ? { ...rebuilt.payload, ...cacheChangesBetween(cacheBefore, cachedEntries(projectPath)) } : { rebuilt: false, problem: rebuilt.message };
57540
57642
  ok &&= rebuilt.ok;
57541
57643
  }
57542
57644
  if (changed.length > 0) {
@@ -58102,7 +58204,7 @@ async function runGodotServer() {
58102
58204
  const server = new GodotServer;
58103
58205
  await server.run();
58104
58206
  }
58105
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, SLOW_ANSWER_MS = 1000, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_TIMEOUT_MS = 600000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. 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 listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", RAISED_WARNING = " (Warning treated as error.)", PARSE_ERROR2 = "Parse Error: ", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
58207
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, FAILED_BUDGET = 1e6, ANSWER_BUDGET = 4000000, __dirname2, PARENT_ASKED_AFTER_MS = 5000, EDITOR_SERVERS_START_MS = 20000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, ALONGSIDE_NOTE = "Another call on this game was running while this one was: calls sent together run together and are answered as each finishes, so this answer is not from after the other. Send a call meant to come after another once that one has answered.", OWN_DEBUGGER = " Its debugger is the keeper holding it rather than the editor, so the debug_* tools answer for it all the same: breakpoints stop it, and debug_control pause breaks into it wherever it is, which the editor offers no way to do.", PLAY_STARTS_WITHIN_MS = 30000, KEEPER_ANSWER_MS = 15000, KEEPER_DEBUG_MS, EXIT_REPORTED_WITHIN_MS = 2000, SUCCESSOR_SOUGHT_WITHIN_MS = 20000, CLOSE_ASK_MS = 3000, LONGEST_BOOT_NOTED_MS, PROFILE_SEND_MS = 5000, MOST_GROUPS = 20, RUNTIME_LISTENING_LINE = "[gdharness] runtime listening on ", VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, CONSOLE_WATCH_MS = 250, BOUNDARY_WAIT_MS = 2000, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, STREAMS_GRACE_MS = 2000, PLAYING_STATUS_MS = 1000, SCAN_SETTLED_MS = 500, SLOW_ANSWER_MS = 1000, REIMPORT_TIMEOUT_MS = 300000, OPS_ELSEWHERE, AUDIO_BUS_OPS, STALLED_AFTER_MS = 30000, TEST_TIMEOUT_MS = 600000, TEST_HEARTBEAT_MS = 20000, DEPENDED_FAILED = "Failed to compile depended scripts", LOOPING_AFTER = 100, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, DESTINATION_ARGUMENTS, OUTPUT_PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/", PROFILE_FUNCTIONS = 30, PROFILER_ON_MS = 3000, PROPERTY_PLACES = "It is a property, so each place says whether it writes it (access write), which runs its set block, or reads it (access read), which runs its get block: withinCalls on a read says nothing of the set block, nor on a write of the get block unless the write is a compound one such as +=. 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 listed.", CALLERS_FOUND_BY_NAME = "Found by name in the project's scripts, since the engine's profiler counts calls and time per function and records nothing about who made them: a method of the same name on another class is listed too, unless one class was asked about and it is called through another class by name or through a variable declared as one. Each place carries the profile's numbers for the function it is written in.", MOST_ENGINE_ENTRIES = 200, MOST_PRINTED_LINES = 50, MOST_UNDECLARED_ERRORS = 20, ERROR_ASSERT = "GdUnitGodotErrorAssertImpl.gd", RAISED_WARNING = " (Warning treated as error.)", PARSE_ERROR2 = "Parse Error: ", EXTERNAL_MEMBER = 'Could not resolve external class member "', STATIC_LIST_HINT;
58106
58208
  var init_server = __esm(() => {
58107
58209
  init_dist2();
58108
58210
  init_stdio();
@@ -58171,7 +58273,6 @@ var init_server = __esm(() => {
58171
58273
  init_update_check();
58172
58274
  __dirname2 = dirname25(fileURLToPath5(import.meta.url));
58173
58275
  KEEPER_DEBUG_MS = PAUSE_MS + STEP_MS + 2 * READ_MS;
58174
- CONSOLE_WRAPPER = /_console(\.exe)?$/i;
58175
58276
  LONGEST_BOOT_NOTED_MS = 5 * 60000;
58176
58277
  VALUE_NAMES = {
58177
58278
  string: "a string",
@@ -58461,6 +58562,7 @@ async function pipe(port) {
58461
58562
 
58462
58563
  // src/cli.ts
58463
58564
  init_paths();
58565
+ init_process_children();
58464
58566
 
58465
58567
  // src/prompt.ts
58466
58568
  import process12 from "node:process";
@@ -60317,10 +60419,8 @@ async function upgrade() {
60317
60419
  }
60318
60420
  console.log(`
60319
60421
  On ${version}. What is left for you:`);
60320
- console.log(digestBefore !== null && digestBefore === installedEditorDigest(projectPath) ? ` 1. Nothing for the editor: this upgrade left the editor addons the same code as before,
60321
- ` + ` so it gives an open editor nothing to restart for. editor_status says addonIsStale
60322
- ` + " if the editor is running other code." : ` 1. The open editor is still running the addons it loaded at startup. Restart it with the
60323
- ` + " editor_launch restart tool, which closes and reopens the window.");
60422
+ const tree = await processTree();
60423
+ console.log(editorUpgradeNote(digestBefore !== null && digestBefore === installedEditorDigest(projectPath), tree === undefined ? null : editorsOfProjectIn(tree, projectPath)));
60324
60424
  const serving = await serverServing(projectPath);
60325
60425
  console.log(harnessNote({ moved, written, byHand }, [launch.command, ...launch.args].join(" "), serving === null ? null : { server: serving, isThisVersion: serving.version === version }));
60326
60426
  const trailing = worktreeCheckoutsOf(projectPath).flatMap((path) => {