gdharness 1.1.4 → 1.1.6

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
@@ -10190,19 +10190,19 @@ import {
10190
10190
  closeSync as closeSync2,
10191
10191
  constants,
10192
10192
  copyFileSync,
10193
- existsSync as existsSync10,
10193
+ existsSync as existsSync11,
10194
10194
  fstatSync,
10195
10195
  mkdirSync as mkdirSync10,
10196
10196
  mkdtempSync as mkdtempSync2,
10197
10197
  openSync as openSync3,
10198
10198
  readdirSync as readdirSync7,
10199
- readFileSync as readFileSync16,
10199
+ readFileSync as readFileSync17,
10200
10200
  readSync as readSync2,
10201
10201
  rmdirSync,
10202
- statSync as statSync7
10202
+ statSync as statSync8
10203
10203
  } from "node:fs";
10204
10204
  import { tmpdir as tmpdir4 } from "node:os";
10205
- import { basename as basename3, dirname as dirname11, extname as extname2, isAbsolute as isAbsolute3, join as join16, normalize as normalize2, relative as relative3 } from "node:path";
10205
+ import { basename as basename4, dirname as dirname11, extname as extname2, isAbsolute as isAbsolute3, join as join17, normalize as normalize2, relative as relative3 } from "node:path";
10206
10206
  import { setTimeout as delay3 } from "node:timers/promises";
10207
10207
  import { fileURLToPath as fileURLToPath4 } from "node:url";
10208
10208
  import { promisify as promisify5 } from "node:util";
@@ -24811,6 +24811,7 @@ function runArguments(options) {
24811
24811
  function editorArguments(projectPath, ports, logFile) {
24812
24812
  return [
24813
24813
  "-e",
24814
+ "--headless",
24814
24815
  "--path",
24815
24816
  projectPath,
24816
24817
  "--lsp-port",
@@ -26748,6 +26749,9 @@ async function handleDAPTool(client, toolName, args) {
26748
26749
  }
26749
26750
  }
26750
26751
 
26752
+ // src/desktop.ts
26753
+ var HIDDEN_DESKTOP = "gdharness";
26754
+
26751
26755
  // src/dictionary.ts
26752
26756
  function emptyRecord() {
26753
26757
  return Object.create(null);
@@ -27921,7 +27925,6 @@ var HEADLESS_OPERATIONS = dictionary({
27921
27925
  status: "get_import_status",
27922
27926
  options: "get_import_options",
27923
27927
  set_options: "set_import_options",
27924
- reimport: "reimport_resource",
27925
27928
  uid: "get_uid",
27926
27929
  refresh_classes: "refresh_class_cache"
27927
27930
  }),
@@ -27935,7 +27938,7 @@ var HEADLESS_OPERATIONS = dictionary({
27935
27938
  })
27936
27939
  });
27937
27940
  var ENGINE_PASSES = dictionary({
27938
- project_import: dictionary({ refresh_uids: "import" })
27941
+ project_import: dictionary({ refresh_uids: "import", reimport: "import" })
27939
27942
  });
27940
27943
  var EDITOR_READS = dictionary({
27941
27944
  project_settings: dictionary({ get: "get_project_setting" })
@@ -28936,7 +28939,8 @@ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = K
28936
28939
  command: spec.command,
28937
28940
  args: spec.args,
28938
28941
  envChanges: changesFrom(spec.env, process.env),
28939
- ...spec.run === undefined ? {} : { run: spec.run }
28942
+ ...spec.run === undefined ? {} : { run: spec.run },
28943
+ ...spec.desktop === undefined ? {} : { desktop: spec.desktop }
28940
28944
  });
28941
28945
  let printed = "";
28942
28946
  let complained = "";
@@ -29121,8 +29125,28 @@ function searchProject(projectPath, options) {
29121
29125
  return result;
29122
29126
  }
29123
29127
 
29128
+ // src/reimport.ts
29129
+ import { createHash as createHash3 } from "node:crypto";
29130
+ import { existsSync as existsSync9, readFileSync as readFileSync12, rmSync as rmSync5, statSync as statSync5, utimesSync, writeFileSync as writeFileSync8 } from "node:fs";
29131
+ import { basename as basename2, join as join13 } from "node:path";
29132
+ function forceNextImport(projectPath, resourcePath) {
29133
+ const relative = resourcePath.replace(/^res:\/\//, "");
29134
+ const sidecar = join13(projectPath, `${relative}.import`);
29135
+ if (!existsSync9(sidecar)) {
29136
+ return;
29137
+ }
29138
+ const { atime, mtimeMs } = statSync5(sidecar);
29139
+ writeFileSync8(sidecar, readFileSync12(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
29140
+ utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
29141
+ rmSync5(importRecord(projectPath, resourcePath), { force: true });
29142
+ }
29143
+ function importRecord(projectPath, resourcePath) {
29144
+ const digest = createHash3("md5").update(resourcePath).digest("hex");
29145
+ return join13(projectPath, ".godot", "imported", `${basename2(resourcePath)}-${digest}.md5`);
29146
+ }
29147
+
29124
29148
  // src/resources.ts
29125
- import { readFileSync as readFileSync12 } from "node:fs";
29149
+ import { readFileSync as readFileSync13 } from "node:fs";
29126
29150
  import { extname, resolve as resolve4 } from "node:path";
29127
29151
  var STATIC_RESOURCES = [
29128
29152
  {
@@ -29317,7 +29341,7 @@ function readResourceText(uri, getProjectPath) {
29317
29341
  const parsedUri = parseGodotUri(uri);
29318
29342
  if (parsedUri.kind === "project-info") {
29319
29343
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
29320
- const rawProject = readFileSync12(projectFilePath, "utf-8");
29344
+ const rawProject = readFileSync13(projectFilePath, "utf-8");
29321
29345
  const parsedProject = parseProjectGodot(rawProject);
29322
29346
  return {
29323
29347
  mimeType: "application/json",
@@ -29326,7 +29350,7 @@ function readResourceText(uri, getProjectPath) {
29326
29350
  }
29327
29351
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
29328
29352
  ensureAllowedExtension(parsedUri.kind, filePath);
29329
- const text = readFileSync12(filePath, "utf-8");
29353
+ const text = readFileSync13(filePath, "utf-8");
29330
29354
  return {
29331
29355
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
29332
29356
  text
@@ -29363,16 +29387,16 @@ function setupResourceHandlers(mcp, getProjectPath) {
29363
29387
  }
29364
29388
 
29365
29389
  // src/restart-note.ts
29366
- import { mkdirSync as mkdirSync7, readFileSync as readFileSync13, rmSync as rmSync5, writeFileSync as writeFileSync8 } from "node:fs";
29367
- import { dirname as dirname8, join as join13 } from "node:path";
29390
+ import { mkdirSync as mkdirSync7, readFileSync as readFileSync14, rmSync as rmSync6, writeFileSync as writeFileSync9 } from "node:fs";
29391
+ import { dirname as dirname8, join as join14 } from "node:path";
29368
29392
  function restartNotePath(projectPath) {
29369
- return join13(projectPath, ".godot", "gdharness-restart.json");
29393
+ return join14(projectPath, ".godot", "gdharness-restart.json");
29370
29394
  }
29371
29395
  function noteRestartBegun(note) {
29372
29396
  const path = restartNotePath(note.projectPath);
29373
29397
  try {
29374
29398
  mkdirSync7(dirname8(path), { recursive: true });
29375
- writeFileSync8(path, `${JSON.stringify(note, null, 2)}
29399
+ writeFileSync9(path, `${JSON.stringify(note, null, 2)}
29376
29400
  `, "utf8");
29377
29401
  } catch (error) {
29378
29402
  console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
@@ -29380,7 +29404,7 @@ function noteRestartBegun(note) {
29380
29404
  }
29381
29405
  function restartOwed(projectPath) {
29382
29406
  try {
29383
- const parsed = JSON.parse(readFileSync13(restartNotePath(projectPath), "utf8"));
29407
+ const parsed = JSON.parse(readFileSync14(restartNotePath(projectPath), "utf8"));
29384
29408
  if (typeof parsed !== "object" || parsed === null) {
29385
29409
  return null;
29386
29410
  }
@@ -29395,7 +29419,7 @@ function restartOwed(projectPath) {
29395
29419
  }
29396
29420
  function restartSettled(projectPath) {
29397
29421
  try {
29398
- rmSync5(restartNotePath(projectPath), { force: true });
29422
+ rmSync6(restartNotePath(projectPath), { force: true });
29399
29423
  } catch (error) {
29400
29424
  console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
29401
29425
  }
@@ -29403,33 +29427,33 @@ function restartSettled(projectPath) {
29403
29427
 
29404
29428
  // src/run-record.ts
29405
29429
  import { execFileSync as execFileSync2 } from "node:child_process";
29406
- import { createHash as createHash3 } from "node:crypto";
29430
+ import { createHash as createHash4 } from "node:crypto";
29407
29431
  import {
29408
29432
  mkdirSync as mkdirSync8,
29409
29433
  openSync as openSync2,
29410
29434
  readdirSync as readdirSync5,
29411
- readFileSync as readFileSync14,
29435
+ readFileSync as readFileSync15,
29412
29436
  realpathSync as realpathSync3,
29413
- rmSync as rmSync6,
29414
- statSync as statSync5,
29415
- writeFileSync as writeFileSync9
29437
+ rmSync as rmSync7,
29438
+ statSync as statSync6,
29439
+ writeFileSync as writeFileSync10
29416
29440
  } from "node:fs";
29417
- import { basename as basename2, dirname as dirname9, join as join14, resolve as resolve5 } from "node:path";
29441
+ import { basename as basename3, dirname as dirname9, join as join15, resolve as resolve5 } from "node:path";
29418
29442
  function runsDirectory() {
29419
- return join14(runtimeDirectory(), "runs");
29443
+ return join15(runtimeDirectory(), "runs");
29420
29444
  }
29421
29445
  function realAsFarAsItExists(path) {
29422
29446
  const missing = [];
29423
29447
  let at = path;
29424
29448
  for (;; ) {
29425
29449
  try {
29426
- return join14(realpathSync3.native(at), ...missing);
29450
+ return join15(realpathSync3.native(at), ...missing);
29427
29451
  } catch {
29428
29452
  const parent = dirname9(at);
29429
29453
  if (parent === at) {
29430
29454
  return path;
29431
29455
  }
29432
- missing.unshift(basename2(at));
29456
+ missing.unshift(basename3(at));
29433
29457
  at = parent;
29434
29458
  }
29435
29459
  }
@@ -29437,27 +29461,27 @@ function realAsFarAsItExists(path) {
29437
29461
  function projectKey(projectPath) {
29438
29462
  const real = realAsFarAsItExists(resolve5(projectPath)).replace(/[\\/]+$/, "");
29439
29463
  const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
29440
- return createHash3("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
29464
+ return createHash4("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
29441
29465
  }
29442
29466
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
29443
- return join14(directory, "runs", `note-${projectKey(projectPath)}.json`);
29467
+ return join15(directory, "runs", `note-${projectKey(projectPath)}.json`);
29444
29468
  }
29445
29469
  var SHARED_NOTE = "run.json";
29446
29470
  function openTranscript(startedAt) {
29447
29471
  mkdirSync8(runsDirectory(), { recursive: true });
29448
- const path = join14(runsDirectory(), `run-${startedAt}.log`);
29472
+ const path = join15(runsDirectory(), `run-${startedAt}.log`);
29449
29473
  return { path, fd: openSync2(path, "a") };
29450
29474
  }
29451
29475
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
29452
- return join14(directory, "runs", `editor-${projectKey(projectPath)}.json`);
29476
+ return join15(directory, "runs", `editor-${projectKey(projectPath)}.json`);
29453
29477
  }
29454
29478
  function writeEditorRunNote(note) {
29455
29479
  mkdirSync8(runsDirectory(), { recursive: true });
29456
- writeFileSync9(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
29480
+ writeFileSync10(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
29457
29481
  }
29458
29482
  function readEditorRunNote(projectPath) {
29459
29483
  for (const directory of runtimeDirectories()) {
29460
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join14(directory, "runs", "editor-run.json"));
29484
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join15(directory, "runs", "editor-run.json"));
29461
29485
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
29462
29486
  return note;
29463
29487
  }
@@ -29467,7 +29491,7 @@ function readEditorRunNote(projectPath) {
29467
29491
  function editorNoteAt(path) {
29468
29492
  let parsed;
29469
29493
  try {
29470
- parsed = JSON.parse(readFileSync14(path, "utf8"));
29494
+ parsed = JSON.parse(readFileSync15(path, "utf8"));
29471
29495
  } catch {
29472
29496
  return null;
29473
29497
  }
@@ -29489,7 +29513,7 @@ function readRunRecord(projectPath) {
29489
29513
  if (own !== null) {
29490
29514
  return own;
29491
29515
  }
29492
- const shared = recordAt(join14(directory, "runs", SHARED_NOTE));
29516
+ const shared = recordAt(join15(directory, "runs", SHARED_NOTE));
29493
29517
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
29494
29518
  return shared;
29495
29519
  }
@@ -29501,13 +29525,13 @@ function everyRunRecord() {
29501
29525
  for (const directory of runtimeDirectories()) {
29502
29526
  let entries;
29503
29527
  try {
29504
- entries = readdirSync5(join14(directory, "runs"));
29528
+ entries = readdirSync5(join15(directory, "runs"));
29505
29529
  } catch {
29506
29530
  continue;
29507
29531
  }
29508
29532
  for (const entry of entries) {
29509
29533
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
29510
- const record = recordAt(join14(directory, "runs", entry));
29534
+ const record = recordAt(join15(directory, "runs", entry));
29511
29535
  if (record !== null) {
29512
29536
  found.push(record);
29513
29537
  }
@@ -29519,7 +29543,7 @@ function everyRunRecord() {
29519
29543
  function recordAt(path) {
29520
29544
  let parsed;
29521
29545
  try {
29522
- parsed = JSON.parse(readFileSync14(path, "utf8"));
29546
+ parsed = JSON.parse(readFileSync15(path, "utf8"));
29523
29547
  } catch {
29524
29548
  return null;
29525
29549
  }
@@ -29565,7 +29589,7 @@ function runningAs(pid) {
29565
29589
  const began = startedAt(pid, null);
29566
29590
  const when = began === null ? {} : { startedAt: began };
29567
29591
  if (process.platform === "linux") {
29568
- const raw = readFileSync14(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
29592
+ const raw = readFileSync15(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
29569
29593
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
29570
29594
  }
29571
29595
  const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
@@ -29668,7 +29692,7 @@ function judgeRun(record, running, needed) {
29668
29692
  if (running.startedAt !== undefined && running.startedAt - record.startedAt > SAME_RUN_WINDOW_MS) {
29669
29693
  return false;
29670
29694
  }
29671
- const engine = record.command === undefined ? null : basename2(record.command);
29695
+ const engine = record.command === undefined ? null : basename3(record.command);
29672
29696
  const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
29673
29697
  const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
29674
29698
  if (wanted !== null && !said.includes(wanted)) {
@@ -29704,19 +29728,19 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
29704
29728
  }
29705
29729
  function clearRunRecord(projectPath) {
29706
29730
  for (const directory of runtimeDirectories()) {
29707
- const shared = join14(directory, "runs", SHARED_NOTE);
29731
+ const shared = join15(directory, "runs", SHARED_NOTE);
29708
29732
  const sharedRecord = recordAt(shared);
29709
29733
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
29710
29734
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
29711
29735
  try {
29712
- rmSync6(path, { force: true });
29736
+ rmSync7(path, { force: true });
29713
29737
  } catch {}
29714
29738
  }
29715
29739
  }
29716
29740
  }
29717
29741
  function ageMs(path, now) {
29718
29742
  try {
29719
- return now - statSync5(path).mtimeMs;
29743
+ return now - statSync6(path).mtimeMs;
29720
29744
  } catch {
29721
29745
  return 0;
29722
29746
  }
@@ -29734,29 +29758,29 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
29734
29758
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
29735
29759
  continue;
29736
29760
  }
29737
- const path = join14(directory, entry);
29761
+ const path = join15(directory, entry);
29738
29762
  if (kept.has(path) || ageMs(path, now) < keepMs) {
29739
29763
  continue;
29740
29764
  }
29741
29765
  try {
29742
- rmSync6(path);
29766
+ rmSync7(path);
29743
29767
  } catch {}
29744
29768
  }
29745
29769
  }
29746
29770
 
29747
29771
  // src/setup.ts
29748
- import { createHash as createHash4 } from "node:crypto";
29772
+ import { createHash as createHash5 } from "node:crypto";
29749
29773
  import {
29750
29774
  cpSync,
29751
- existsSync as existsSync9,
29775
+ existsSync as existsSync10,
29752
29776
  mkdirSync as mkdirSync9,
29753
29777
  readdirSync as readdirSync6,
29754
- readFileSync as readFileSync15,
29755
- rmSync as rmSync7,
29756
- statSync as statSync6,
29757
- writeFileSync as writeFileSync10
29778
+ readFileSync as readFileSync16,
29779
+ rmSync as rmSync8,
29780
+ statSync as statSync7,
29781
+ writeFileSync as writeFileSync11
29758
29782
  } from "node:fs";
29759
- import { dirname as dirname10, join as join15 } from "node:path";
29783
+ import { dirname as dirname10, join as join16 } from "node:path";
29760
29784
  import { fileURLToPath as fileURLToPath3 } from "node:url";
29761
29785
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
29762
29786
  var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
@@ -29767,27 +29791,27 @@ var RUNTIME_AUTOLOAD = {
29767
29791
  var VERSION_MARKER = ".gdharness-version";
29768
29792
  var DIGEST_MARKER = ".gdharness-digest";
29769
29793
  function editorAddonDigest(from = shippedAddonsDirectory()) {
29770
- const hash = createHash4("sha256");
29794
+ const hash = createHash5("sha256");
29771
29795
  const walk = (directory, relative) => {
29772
29796
  for (const name of readdirSync6(directory).sort()) {
29773
29797
  if (name === VERSION_MARKER || name === DIGEST_MARKER) {
29774
29798
  continue;
29775
29799
  }
29776
- const path = join15(directory, name);
29800
+ const path = join16(directory, name);
29777
29801
  const inside = `${relative}/${name}`;
29778
- if (statSync6(path).isDirectory()) {
29802
+ if (statSync7(path).isDirectory()) {
29779
29803
  walk(path, inside);
29780
29804
  } else {
29781
29805
  hash.update(`${inside}
29782
29806
  `);
29783
- hash.update(readFileSync15(path));
29807
+ hash.update(readFileSync16(path));
29784
29808
  hash.update(`
29785
29809
  `);
29786
29810
  }
29787
29811
  }
29788
29812
  };
29789
29813
  for (const name of EDITOR_PLUGINS) {
29790
- walk(join15(from, name), name);
29814
+ walk(join16(from, name), name);
29791
29815
  }
29792
29816
  return hash.digest("hex");
29793
29817
  }
@@ -29803,14 +29827,14 @@ function shippedEditorDigest() {
29803
29827
  return shippedDigest ?? undefined;
29804
29828
  }
29805
29829
  function shippedAddonsDirectory() {
29806
- return join15(dirname10(fileURLToPath3(import.meta.url)), "godot", "addons");
29830
+ return join16(dirname10(fileURLToPath3(import.meta.url)), "godot", "addons");
29807
29831
  }
29808
29832
  function installedAddonVersion(projectPath) {
29809
- const marker = join15(projectPath, "addons", ADDONS[0], VERSION_MARKER);
29810
- if (!existsSync9(marker)) {
29833
+ const marker = join16(projectPath, "addons", ADDONS[0], VERSION_MARKER);
29834
+ if (!existsSync10(marker)) {
29811
29835
  return null;
29812
29836
  }
29813
- const said = readFileSync15(marker, "utf8").trim();
29837
+ const said = readFileSync16(marker, "utf8").trim();
29814
29838
  return said === "" ? null : said;
29815
29839
  }
29816
29840
 
@@ -30001,7 +30025,7 @@ var TOOL_SPECS = [
30001
30025
  },
30002
30026
  {
30003
30027
  name: "project_settings",
30004
- description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses.",
30028
+ 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 from the file (named under editorAdopted), 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.",
30005
30029
  parameters: {
30006
30030
  projectPath: PROJECT_PATH,
30007
30031
  setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
@@ -30113,19 +30137,30 @@ var TOOL_SPECS = [
30113
30137
  description: 'set_options: import options keyed as the .import file spells them, {"compress/mode": 1}.',
30114
30138
  additionalProperties: true
30115
30139
  },
30116
- reimport: { type: "boolean", description: "set_options: reimport afterwards. Default true." },
30117
- force: { type: "boolean", description: "reimport: reimport even what is current. Default false." }
30140
+ reimport: {
30141
+ type: "boolean",
30142
+ description: "set_options: reimport afterwards, answered under reimport as project_import reimport answers. Default true."
30143
+ },
30144
+ force: { type: "boolean", description: "reimport: reimport even what is current. Default false." },
30145
+ timeoutMs: {
30146
+ type: "integer",
30147
+ minimum: 1,
30148
+ description: "reimport, set_options: how long to wait for the editor to finish reimporting. Default 300000."
30149
+ }
30118
30150
  },
30119
30151
  requires: ["projectPath"],
30120
30152
  operations: {
30121
30153
  status: {
30122
- summary: "which resources are outdated or failed, or one resource with resourcePath, judged by content as the editor judges it: a source whose hash differs from the one its import recorded, an import whose output under .godot/imported is gone (missing_outputs), and a glTF scene whose import does not depend on an image it names, because it was built before that image was imported (imported_without), each with its reason",
30154
+ summary: "which resources are outdated or failed, or one resource with resourcePath, each with its reason. Judged by content with the checks the editor makes before it reimports: a sidecar changed since its import, as setting an option without a reimport leaves it; a sidecar with no uid or written for another file; an output under .godot/imported that is gone (missing_outputs) or changed; a source whose hash differs from the one its import recorded, or with no record. Beyond the editor, a glTF scene whose import does not depend on an image it names, because it was built before that image was imported (imported_without; a scene whose import runs an import script is not judged on images). failed is an import that failed, which the editor never tries again on its own. Without resourcePath, a sidecar whose source is gone is listed as missing_source",
30123
30155
  requires: []
30124
30156
  },
30125
30157
  options: { summary: "the import options of one resource", requires: ["resourcePath"] },
30126
- set_options: { summary: "change import options", requires: ["resourcePath", "options"] },
30158
+ set_options: {
30159
+ summary: "change import options in the sidecar, then apply them with a reimport of that resource",
30160
+ requires: ["resourcePath", "options"]
30161
+ },
30127
30162
  reimport: {
30128
- summary: "reimport one resource, or everything modified without resourcePath",
30163
+ summary: "reimport what status says is outdated or failed, one resource with resourcePath, or what is current as well with force. Through the open editor when one serves the project, so it reloads what it holds, waiting for it to finish; otherwise through the engine's own import pass. Answered from a second status read: reimported lists what is current now, notReimported what is not, with its status and reason, and via says which of the two did the work. stillImporting is an editor that had not finished within timeoutMs",
30129
30164
  requires: []
30130
30165
  },
30131
30166
  uid: { summary: "the UID of one file", requires: ["resourcePath"] },
@@ -30427,7 +30462,7 @@ var TOOL_SPECS = [
30427
30462
  },
30428
30463
  {
30429
30464
  name: "editor_launch",
30430
- description: "Opens the Godot editor on a project, in a window on this machine, or restarts the one already connected. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. Only an editor with a window can be restarted, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart.",
30465
+ description: "Opens the Godot editor on a project, or restarts the one already connected. An editor this opens is headless: it shows no window and takes no focus, and it serves the bridge, the language server, the debug adapter and the games it plays as a windowed one does. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect; it saves open scenes on the way out and answers with the version that came back. An editor a server opened is started again by the server, headless as before. One opened by hand restarts itself, which only one with a window can do, because the engine hands back none of the arguments it was started with. editor_status says which editor is connected and whether it is holding an old addon. Opening a project is a save and so is closing one, which is the part that is not obvious from outside: the editor imports the project on open and writes project.godot back, and Godot writes only what differs from its own defaults, so a key named deliberately at its default value is dropped either way. A restart says which under settingsDropped in its own answer because it waits for the editor to return. An open cannot, since it answers as soon as the process exists and the save happens during the import minutes later, so editor_status carries that reading once the editor has connected. Nothing else will say a key went until something depends on one. A restart of an editor this server opened is a quit and then a launch, and this server can be ended between them, usually by the reconnect the restart was being taken for: the editor is gone and nothing replaces it. That is written down before the quit, so the next server says so under restartInterrupted in editor_status rather than waiting for an editor that is not coming, and open finishes it, saying so under finishesRestart.",
30431
30466
  parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
30432
30467
  requires: [],
30433
30468
  operations: {
@@ -30453,6 +30488,11 @@ var TOOL_SPECS = [
30453
30488
  description: `The game's own arguments, what OS.get_cmdline_user_args() answers, such as ["--level=2"]. The separator is added here. A run with any is started by this server rather than by the editor, which fixes the game's command line when it opens the project, so the debug_* tools do not answer for it.`
30454
30489
  },
30455
30490
  headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
30491
+ visible: {
30492
+ type: "boolean",
30493
+ ops: ["start"],
30494
+ description: "start: on Windows, show the window on the desktop in use. By default a windowed run this server starts goes on a desktop of its own, where it renders and runtime_capture works but the window never shows and never takes the keyboard. A game the editor plays goes there too, since an editor this server opens is on that desktop. Elsewhere than Windows the window shows either way. Default false."
30495
+ },
30456
30496
  savesIn: {
30457
30497
  type: "string",
30458
30498
  ops: ["start", "check"],
@@ -30783,7 +30823,7 @@ var TOOL_SPECS = [
30783
30823
  },
30784
30824
  {
30785
30825
  name: "runtime_capture",
30786
- description: "A picture of the running game: the whole screen or one viewport, as an image. Needs the game running with a window.",
30826
+ description: "A picture of the running game: the whole screen or one viewport, as an image. Needs the game running with a window. A capture asked for before the game's first frame is drawn waits for that frame, since the game announces itself before it and the viewport holds nothing it drew until then.",
30787
30827
  parameters: {
30788
30828
  projectPath: RUNNING_PROJECT_PATH,
30789
30829
  pid: RUNNING_PID,
@@ -31091,15 +31131,15 @@ function captureDestinationRefusal(outputPath, projects) {
31091
31131
  return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
31092
31132
  }
31093
31133
  const directory = realPathOr(dirname11(outputPath));
31094
- if (!existsSync10(directory)) {
31134
+ if (!existsSync11(directory)) {
31095
31135
  return `outputPath's directory ${dirname11(outputPath)} does not exist; a capture creates no directories.`;
31096
31136
  }
31097
- const where = join16(directory, basename3(outputPath));
31137
+ const where = join17(directory, basename4(outputPath));
31098
31138
  const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
31099
31139
  if (inside !== undefined) {
31100
31140
  return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
31101
31141
  }
31102
- if (existsSync10(where)) {
31142
+ if (existsSync11(where)) {
31103
31143
  return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
31104
31144
  }
31105
31145
  return null;
@@ -31477,6 +31517,34 @@ function scanWaitAnswer(scanned) {
31477
31517
  var PLAYING_STATUS_MS = 1000;
31478
31518
  var SCAN_START_MS = 2000;
31479
31519
  var SCAN_SETTLED_MS = 500;
31520
+ var REIMPORT_TIMEOUT_MS = 300000;
31521
+ var AUDIO_BUS_OPS = new Set([
31522
+ "add_audio_bus",
31523
+ "set_audio_bus_effect",
31524
+ "set_audio_bus_volume"
31525
+ ]);
31526
+ function changedSettings(before, after) {
31527
+ if (before === null || after === null) {
31528
+ return null;
31529
+ }
31530
+ const names = new Set([...before.keys(), ...after.keys()]);
31531
+ return [...names].filter((name) => JSON.stringify(before.get(name)) !== JSON.stringify(after.get(name))).sort();
31532
+ }
31533
+ function importStatuses(payload) {
31534
+ const listed = payload["resources"];
31535
+ if (!Array.isArray(listed)) {
31536
+ return [];
31537
+ }
31538
+ return listed.map((entry) => {
31539
+ const one = asParams(entry);
31540
+ const reason = readString(one, "reason");
31541
+ return {
31542
+ path: readString(one, "path") ?? "",
31543
+ status: readString(one, "status") ?? "unknown",
31544
+ ...reason === undefined ? {} : { reason }
31545
+ };
31546
+ });
31547
+ }
31480
31548
  var WAIT_FOR_RUN_MS = 600000;
31481
31549
  var RUN_POLL_MS = 250;
31482
31550
  var PATH_SOLUTIONS = [
@@ -31538,7 +31606,7 @@ function aboveTheRunner(entry) {
31538
31606
  }
31539
31607
  function elsewhereIn(projectPath, asked) {
31540
31608
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
31541
- if (wanted === "" || existsSync10(join16(projectPath, wanted))) {
31609
+ if (wanted === "" || existsSync11(join17(projectPath, wanted))) {
31542
31610
  return "";
31543
31611
  }
31544
31612
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -31552,7 +31620,7 @@ function elsewhereIn(projectPath, asked) {
31552
31620
  return near.length === 0 ? "" : ` The project does have ${there}.`;
31553
31621
  }
31554
31622
  function hasMainScene(projectFile) {
31555
- const scene = parseProjectGodot(readFileSync16(projectFile, "utf8"))["application"]?.["run/main_scene"];
31623
+ const scene = parseProjectGodot(readFileSync17(projectFile, "utf8"))["application"]?.["run/main_scene"];
31556
31624
  return typeof scene === "string" && scene !== "";
31557
31625
  }
31558
31626
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -31560,13 +31628,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
31560
31628
  return false;
31561
31629
  }
31562
31630
  try {
31563
- return readFileSync16(join16(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
31631
+ return readFileSync17(join17(projectPath, RUNTIME_AUTOLOAD.path), "utf8").includes('"editor_pid"');
31564
31632
  } catch {
31565
31633
  return false;
31566
31634
  }
31567
31635
  }
31568
31636
  function editorPlaysHeadless(projectFile) {
31569
- const runArgs = parseProjectGodot(readFileSync16(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
31637
+ const runArgs = parseProjectGodot(readFileSync17(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
31570
31638
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
31571
31639
  }
31572
31640
  function camelCased(params) {
@@ -31585,7 +31653,7 @@ function needsItsOwnProcess(asked) {
31585
31653
  class GodotServer {
31586
31654
  mcp;
31587
31655
  locator = new GodotLocator;
31588
- operationsScript = join16(__dirname2, "godot", "operations", "godot_operations.gd");
31656
+ operationsScript = join17(__dirname2, "godot", "operations", "godot_operations.gd");
31589
31657
  godotBridge;
31590
31658
  tools = buildToolDefinitions();
31591
31659
  activeProcess = null;
@@ -32030,7 +32098,14 @@ class GodotServer {
32030
32098
  }
32031
32099
  async dispatch(tool, op, args) {
32032
32100
  if (ENGINE_PASSES[tool]?.[op] !== undefined) {
32033
- return await this.handleRefreshUids(args);
32101
+ return op === "reimport" ? await this.handleReimport(args) : await this.handleRefreshUids(args);
32102
+ }
32103
+ const settingsWrite = tool === "project_settings" && op !== "get" ? HEADLESS_OPERATIONS[tool]?.[op] : undefined;
32104
+ if (settingsWrite !== undefined) {
32105
+ return await this.handleProjectSettingsWrite(op, settingsWrite, args);
32106
+ }
32107
+ if (tool === "project_import" && op === "set_options") {
32108
+ return await this.handleSetImportOptions(args);
32034
32109
  }
32035
32110
  const headless = HEADLESS_OPERATIONS[tool]?.[op];
32036
32111
  if (headless !== undefined) {
@@ -32289,8 +32364,8 @@ class GodotServer {
32289
32364
  if (path === undefined) {
32290
32365
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
32291
32366
  }
32292
- const file = join16(path, "project.godot");
32293
- if (!existsSync10(file)) {
32367
+ const file = join17(path, "project.godot");
32368
+ if (!existsSync11(file)) {
32294
32369
  return {
32295
32370
  ok: false,
32296
32371
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -32561,6 +32636,222 @@ class GodotServer {
32561
32636
  }
32562
32637
  }, scanned));
32563
32638
  }
32639
+ async handleReimport(args) {
32640
+ const project = this.project(args);
32641
+ if (!project.ok) {
32642
+ return project.response;
32643
+ }
32644
+ const contained = this.containProjectFiles(args);
32645
+ if (!contained.ok) {
32646
+ return contained.response;
32647
+ }
32648
+ const outcome = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), args["force"] === true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
32649
+ return outcome.ok ? this.jsonTextResponse(outcome.payload) : outcome.response;
32650
+ }
32651
+ editorHasOpen(projectPath) {
32652
+ const open = this.godotBridge.getStatus().projectPath;
32653
+ return this.godotBridge.isConnected() && open !== undefined && open !== "" && isSameDirectory(open, projectPath);
32654
+ }
32655
+ async handleProjectSettingsWrite(op, operation, args) {
32656
+ const project = this.project(args);
32657
+ if (!project.ok) {
32658
+ return project.response;
32659
+ }
32660
+ const projectPath = project.value.path;
32661
+ const served = this.editorHasOpen(projectPath);
32662
+ const before = served ? this.settingKeysOf(projectPath) : null;
32663
+ const written = await this.headless(operation, args);
32664
+ if (!served || written.isError === true) {
32665
+ return written;
32666
+ }
32667
+ const answer = asParams(JSON.parse(written.content[0]?.text ?? "{}"));
32668
+ if (AUDIO_BUS_OPS.has(op)) {
32669
+ return this.jsonTextResponse({ ...answer, ...await this.editorTakes("adopt_audio_bus_layout", {}) });
32670
+ }
32671
+ const changed = changedSettings(before, this.settingKeysOf(projectPath));
32672
+ if (changed === null) {
32673
+ return this.jsonTextResponse({
32674
+ ...answer,
32675
+ editorNote: "project.godot could not be read on both sides of the write, so what it changed is unknown and the open editor was not told: it may still hold the old values and write them back the next time it saves. editor_launch restart loads the file."
32676
+ });
32677
+ }
32678
+ if (changed.length === 0) {
32679
+ return written;
32680
+ }
32681
+ const taken = await this.editorTakes("adopt_project_settings", { settings: changed });
32682
+ return this.jsonTextResponse({
32683
+ ...answer,
32684
+ ...taken,
32685
+ ...taken["editorNote"] === undefined ? { editorAdopted: changed } : {}
32686
+ });
32687
+ }
32688
+ async editorTakes(command, args) {
32689
+ try {
32690
+ await this.godotBridge.invokeTool(command, args);
32691
+ return {};
32692
+ } catch (error) {
32693
+ const message = errorMessage(error);
32694
+ const why = message.includes("Unknown tool") ? "runs an addon from before writes were handed to it" : `could not take the change (${message})`;
32695
+ return {
32696
+ editorNote: `The open editor ${why}, so it still holds the old values, answers them when asked, and writes them back over the file the next time it saves. editor_launch restart loads the file and the addon this server ships.`
32697
+ };
32698
+ }
32699
+ }
32700
+ async handleSetImportOptions(args) {
32701
+ const project = this.project(args);
32702
+ if (!project.ok) {
32703
+ return project.response;
32704
+ }
32705
+ const contained = this.containProjectFiles(args);
32706
+ if (!contained.ok) {
32707
+ return contained.response;
32708
+ }
32709
+ const {
32710
+ op: _op,
32711
+ projectPath: _projectPath,
32712
+ reimport: _reimport,
32713
+ timeoutMs: _timeoutMs,
32714
+ ...params
32715
+ } = contained.value;
32716
+ const written = await this.operation("set_import_options", params, project.value.path);
32717
+ if (!written.ok || args["reimport"] === false) {
32718
+ return this.answer(written.ok ? {
32719
+ ...written,
32720
+ payload: {
32721
+ ...written.payload,
32722
+ note: "Not reimported: the editor imports it with these options on its next scan, or project_import reimport does now."
32723
+ }
32724
+ } : written);
32725
+ }
32726
+ const applied = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
32727
+ if (!applied.ok) {
32728
+ return applied.response;
32729
+ }
32730
+ return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
32731
+ }
32732
+ async reimport(projectPath, resourcePath, force, timeoutMs) {
32733
+ const ask = resourcePath === undefined ? { includeUpToDate: force } : { resourcePath };
32734
+ const before = await this.operation("get_import_status", ask, projectPath);
32735
+ if (!before.ok) {
32736
+ return { ok: false, response: this.answer(before) };
32737
+ }
32738
+ const listed = importStatuses(before.payload);
32739
+ const missingSource = listed.filter((one) => one.status === "missing_source").map((one) => one.path);
32740
+ if (resourcePath !== undefined && missingSource.length > 0) {
32741
+ return {
32742
+ ok: false,
32743
+ response: this.createErrorResponse(`${missingSource[0]} is not on disk, so there is nothing to import.`)
32744
+ };
32745
+ }
32746
+ const targets = listed.filter((one) => one.status !== "missing_source" && (force || one.status !== "up_to_date")).map((one) => one.path);
32747
+ if (targets.length === 0) {
32748
+ return {
32749
+ ok: true,
32750
+ payload: {
32751
+ reimported: [],
32752
+ notReimported: [],
32753
+ note: resourcePath === undefined ? "Nothing needed reimporting. Pass force to reimport what is current." : `${listed[0]?.path ?? resourcePath} is current. Pass force to reimport it anyway.`
32754
+ }
32755
+ };
32756
+ }
32757
+ const viaEditor = this.editorHasOpen(projectPath);
32758
+ const extra = {};
32759
+ if (viaEditor) {
32760
+ const inEditor = await this.reimportInTheEditor(targets, timeoutMs);
32761
+ if (!inEditor.ok) {
32762
+ return inEditor;
32763
+ }
32764
+ if (!inEditor.finished) {
32765
+ return {
32766
+ ok: true,
32767
+ payload: {
32768
+ via: "editor",
32769
+ stillImporting: true,
32770
+ asked: targets,
32771
+ note: `The editor had not finished after ${timeoutMs} ms. project_import status says what it has done so far; pass a longer timeoutMs to wait it out.`
32772
+ }
32773
+ };
32774
+ }
32775
+ } else {
32776
+ const engine = await this.engine();
32777
+ if (!engine.ok) {
32778
+ return { ok: false, response: engine.response };
32779
+ }
32780
+ for (const target of targets) {
32781
+ forceNextImport(projectPath, target);
32782
+ }
32783
+ const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
32784
+ const imported = await runImport(engine.value, projectPath);
32785
+ if (!imported.ok) {
32786
+ return { ok: false, response: this.answer(withScanWait(imported, scanned)) };
32787
+ }
32788
+ if (imported.messages.length > 0) {
32789
+ extra["engine_messages"] = imported.messages;
32790
+ }
32791
+ }
32792
+ const after = await this.operation("get_import_status", resourcePath === undefined ? { includeUpToDate: true } : { resourcePath }, projectPath);
32793
+ if (!after.ok) {
32794
+ return { ok: false, response: this.answer(after) };
32795
+ }
32796
+ const now = new Map(importStatuses(after.payload).map((one) => [one.path, one]));
32797
+ const reimported = targets.filter((path) => now.get(path)?.status === "up_to_date");
32798
+ const notReimported = targets.filter((path) => now.get(path)?.status !== "up_to_date").map((path) => ({
32799
+ path,
32800
+ status: now.get(path)?.status ?? "unknown",
32801
+ ...now.get(path)?.reason === undefined ? {} : { reason: now.get(path)?.reason }
32802
+ }));
32803
+ return {
32804
+ ok: true,
32805
+ payload: {
32806
+ via: viaEditor ? "editor" : "engine",
32807
+ reimported,
32808
+ notReimported,
32809
+ ...missingSource.length > 0 ? { missingSource } : {},
32810
+ ...extra
32811
+ }
32812
+ };
32813
+ }
32814
+ async reimportInTheEditor(paths, timeoutMs) {
32815
+ const started = Date.now();
32816
+ let completedBefore;
32817
+ for (;; ) {
32818
+ while (Date.now() - started < timeoutMs && await this.editorIsScanningOrImporting()) {
32819
+ await new Promise((settle) => setTimeout(settle, 100));
32820
+ }
32821
+ let answer;
32822
+ try {
32823
+ answer = asParams(await this.godotBridge.invokeTool("reimport_files", { paths: [...paths] }));
32824
+ } catch (error) {
32825
+ const message = errorMessage(error);
32826
+ return {
32827
+ ok: false,
32828
+ response: message.includes("Unknown tool") ? this.createErrorResponse("The editor is running an addon from before reimport_files, so it cannot be asked to reimport.", [
32829
+ "editor_launch restart loads the addon this server ships",
32830
+ "Or close the editor, and project_import reimport runs the engine import instead"
32831
+ ]) : this.createErrorResponse(`The editor answered reimport_files with an error: ${message}`)
32832
+ };
32833
+ }
32834
+ if (answer["started"] !== false) {
32835
+ completedBefore = readNumber(answer, "reimportsCompletedBefore");
32836
+ break;
32837
+ }
32838
+ if (Date.now() - started >= timeoutMs) {
32839
+ return { ok: true, finished: false };
32840
+ }
32841
+ await new Promise((settle) => setTimeout(settle, 100));
32842
+ }
32843
+ while (Date.now() - started < timeoutMs) {
32844
+ await new Promise((settle) => setTimeout(settle, 250));
32845
+ try {
32846
+ const status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
32847
+ const completed = readNumber(status, "reimportsCompleted");
32848
+ if (completedBefore !== undefined && completed !== undefined && completed > completedBefore) {
32849
+ return { ok: true, finished: true };
32850
+ }
32851
+ } catch {}
32852
+ }
32853
+ return { ok: true, finished: false };
32854
+ }
32564
32855
  async operation(operation, params, projectPath) {
32565
32856
  const engine = await this.engine();
32566
32857
  if (!engine.ok) {
@@ -32602,11 +32893,11 @@ class GodotServer {
32602
32893
  if (!engine.ok) {
32603
32894
  return engine.response;
32604
32895
  }
32605
- const application = parseProjectGodot(readFileSync16(project.value.file, "utf8"))["application"] ?? {};
32896
+ const application = parseProjectGodot(readFileSync17(project.value.file, "utf8"))["application"] ?? {};
32606
32897
  const name = application["config/name"];
32607
32898
  const mainScene = application["run/main_scene"];
32608
32899
  const info = {
32609
- name: typeof name === "string" && name !== "" ? name : basename3(project.value.path),
32900
+ name: typeof name === "string" && name !== "" ? name : basename4(project.value.path),
32610
32901
  path: project.value.path,
32611
32902
  mainScene: typeof mainScene === "string" && mainScene !== "" ? mainScene : null,
32612
32903
  godotVersion: await this.godotVersion(engine.value),
@@ -32670,11 +32961,11 @@ class GodotServer {
32670
32961
  return this.createErrorResponse(`Cannot create ${dirname11(output.relativePath)} to export into: ${errorMessage(error)}`);
32671
32962
  }
32672
32963
  const debug = readBoolean(args, "debug") ?? false;
32673
- const exportLogs = mkdtempSync2(join16(tmpdir4(), "gdharness-export-"));
32964
+ const exportLogs = mkdtempSync2(join17(tmpdir4(), "gdharness-export-"));
32674
32965
  const exportArgs = [
32675
32966
  "--headless",
32676
32967
  "--log-file",
32677
- join16(exportLogs, "engine.log"),
32968
+ join17(exportLogs, "engine.log"),
32678
32969
  "--path",
32679
32970
  project.value.path,
32680
32971
  debug ? "--export-debug" : "--export-release",
@@ -32706,7 +32997,7 @@ class GodotServer {
32706
32997
  log.finish();
32707
32998
  const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
32708
32999
  const verdict = {
32709
- exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync10(output.absolutePath),
33000
+ exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync11(output.absolutePath),
32710
33001
  preset,
32711
33002
  outputPath: output.relativePath,
32712
33003
  debug,
@@ -32742,7 +33033,7 @@ class GodotServer {
32742
33033
  return contained.response;
32743
33034
  }
32744
33035
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
32745
- if (!existsSync10(join16(project.value.path, runner))) {
33036
+ if (!existsSync11(join17(project.value.path, runner))) {
32746
33037
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
32747
33038
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
32748
33039
  ]);
@@ -32778,7 +33069,7 @@ class GodotServer {
32778
33069
  ];
32779
33070
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
32780
33071
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
32781
- const userData = mkdtempSync2(join16(tmpdir4(), "gdharness-tests-"));
33072
+ const userData = mkdtempSync2(join17(tmpdir4(), "gdharness-tests-"));
32782
33073
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
32783
33074
  const console2 = [];
32784
33075
  run.process.stdout?.on("data", (data) => {
@@ -32798,14 +33089,14 @@ class GodotServer {
32798
33089
  resolve(false);
32799
33090
  });
32800
33091
  });
32801
- const reportsDir = join16(project.value.path, ".godot", ...ours.split("/"));
33092
+ const reportsDir = join17(project.value.path, ".godot", ...ours.split("/"));
32802
33093
  let report = null;
32803
33094
  let reportProblem = null;
32804
33095
  try {
32805
- const written = existsSync10(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
33096
+ const written = existsSync11(reportsDir) ? readdirSync7(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
32806
33097
  const newest = written.at(-1);
32807
33098
  if (newest !== undefined) {
32808
- report = parseJUnit(readFileSync16(join16(reportsDir, newest, "results.xml"), "utf8"));
33099
+ report = parseJUnit(readFileSync17(join17(reportsDir, newest, "results.xml"), "utf8"));
32809
33100
  }
32810
33101
  } catch (error) {
32811
33102
  reportProblem = errorMessage(error);
@@ -32948,11 +33239,11 @@ class GodotServer {
32948
33239
  const cached = cachedClasses(projectPath);
32949
33240
  const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
32950
33241
  const contained = resolveWithinProject(projectPath, resourcePath);
32951
- if (!contained.ok || !existsSync10(contained.absolutePath)) {
33242
+ if (!contained.ok || !existsSync11(contained.absolutePath)) {
32952
33243
  return null;
32953
33244
  }
32954
33245
  try {
32955
- return readFileSync16(contained.absolutePath, "utf8");
33246
+ return readFileSync17(contained.absolutePath, "utf8");
32956
33247
  } catch {
32957
33248
  return null;
32958
33249
  }
@@ -33002,11 +33293,11 @@ class GodotServer {
33002
33293
  }
33003
33294
  sourceOfClass(projectPath, resourcePath) {
33004
33295
  const contained = resolveWithinProject(projectPath, resourcePath);
33005
- if (!contained.ok || !existsSync10(contained.absolutePath)) {
33296
+ if (!contained.ok || !existsSync11(contained.absolutePath)) {
33006
33297
  return null;
33007
33298
  }
33008
33299
  try {
33009
- return readFileSync16(contained.absolutePath, "utf8");
33300
+ return readFileSync17(contained.absolutePath, "utf8");
33010
33301
  } catch {
33011
33302
  return null;
33012
33303
  }
@@ -33275,7 +33566,7 @@ class GodotServer {
33275
33566
  return null;
33276
33567
  }
33277
33568
  try {
33278
- return settingKeys(readFileSync16(join16(projectPath, "project.godot"), "utf8"));
33569
+ return settingKeys(readFileSync17(join17(projectPath, "project.godot"), "utf8"));
33279
33570
  } catch {
33280
33571
  return null;
33281
33572
  }
@@ -33415,7 +33706,8 @@ class GodotServer {
33415
33706
  GDHARNESS_LSP_PORT: String(ports.lsp),
33416
33707
  GDHARNESS_DAP_PORT: String(ports.dap),
33417
33708
  [OPENED_BY_A_SERVER]: "1"
33418
- }
33709
+ },
33710
+ desktop: HIDDEN_DESKTOP
33419
33711
  });
33420
33712
  if ("error" in launched) {
33421
33713
  return { pid: null, error: launched.error };
@@ -33497,7 +33789,7 @@ class GodotServer {
33497
33789
  userArgs: given.value
33498
33790
  });
33499
33791
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
33500
- const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined);
33792
+ const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined, headless || args["visible"] === true ? undefined : HIDDEN_DESKTOP);
33501
33793
  if ("error" in started) {
33502
33794
  return this.createErrorResponse(`The game could not be started: ${started.error}`, [
33503
33795
  "editor_status says which Godot this server runs and whether it answers"
@@ -33527,7 +33819,7 @@ class GodotServer {
33527
33819
  }
33528
33820
  async runtimeUp(projectPath, before, budget, withArgs) {
33529
33821
  const budgetMs = budget.ms;
33530
- if (!existsSync10(join16(projectPath, RUNTIME_AUTOLOAD.path))) {
33822
+ if (!existsSync11(join17(projectPath, RUNTIME_AUTOLOAD.path))) {
33531
33823
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
33532
33824
  }
33533
33825
  let asking = null;
@@ -33590,7 +33882,7 @@ class GodotServer {
33590
33882
  }
33591
33883
  let announcedAt;
33592
33884
  try {
33593
- announcedAt = statSync7(endpoint.file).mtimeMs;
33885
+ announcedAt = statSync8(endpoint.file).mtimeMs;
33594
33886
  } catch {
33595
33887
  return;
33596
33888
  }
@@ -33837,7 +34129,7 @@ class GodotServer {
33837
34129
  });
33838
34130
  return started;
33839
34131
  }
33840
- async launchKeptGame(godotPath, cmdArgs, projectPath, env) {
34132
+ async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop) {
33841
34133
  const startedAt = Date.now();
33842
34134
  const transcript = openTranscript(startedAt);
33843
34135
  closeSync2(transcript.fd);
@@ -33845,7 +34137,8 @@ class GodotServer {
33845
34137
  command: godotPath,
33846
34138
  args: cmdArgs,
33847
34139
  ...env === undefined ? {} : { env },
33848
- run: { transcript: transcript.path, startedAt, projectPath }
34140
+ run: { transcript: transcript.path, startedAt, projectPath },
34141
+ ...desktop === undefined ? {} : { desktop }
33849
34142
  });
33850
34143
  if ("error" in launched) {
33851
34144
  return launched;
@@ -34340,7 +34633,7 @@ class GodotServer {
34340
34633
  notes.push(endedWithoutACode({
34341
34634
  throughEditor: run.throughEditor,
34342
34635
  announced: announced !== undefined || run.announcedPid !== undefined,
34343
- couldAnnounce: run.projectPath !== null && existsSync10(join16(run.projectPath, RUNTIME_AUTOLOAD.path)),
34636
+ couldAnnounce: run.projectPath !== null && existsSync11(join17(run.projectPath, RUNTIME_AUTOLOAD.path)),
34344
34637
  endedHere: typeof run.endedHere === "string",
34345
34638
  pickedUp: run.pickedUp === true
34346
34639
  }));
@@ -34566,6 +34859,7 @@ class GodotServer {
34566
34859
  const ghosts = blind.stillHeld ?? [];
34567
34860
  let waitedForEditorMs = 0;
34568
34861
  let first;
34862
+ let completedBefore;
34569
34863
  for (;; ) {
34570
34864
  const busySince = Date.now();
34571
34865
  let wasBusy = false;
@@ -34580,8 +34874,9 @@ class GodotServer {
34580
34874
  if (first.isError) {
34581
34875
  return first;
34582
34876
  }
34583
- const declined = asParams(JSON.parse(first.content[0]?.text ?? "{}"))["started"] === false;
34584
- if (!declined || Date.now() - started >= timeoutMs) {
34877
+ const firstAnswer = asParams(JSON.parse(first.content[0]?.text ?? "{}"));
34878
+ completedBefore = readNumber(firstAnswer, "scansCompletedBefore");
34879
+ if (firstAnswer["started"] !== false || Date.now() - started >= timeoutMs) {
34585
34880
  break;
34586
34881
  }
34587
34882
  await new Promise((settle) => setTimeout(settle, 100));
@@ -34592,7 +34887,9 @@ class GodotServer {
34592
34887
  await new Promise((settle) => setTimeout(settle, 100));
34593
34888
  const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
34594
34889
  const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
34595
- busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending;
34890
+ const completed = readNumber(status, "scansCompleted");
34891
+ const notYetOver = completedBefore !== undefined && completed !== undefined && completed <= completedBefore;
34892
+ busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending || notYetOver;
34596
34893
  if (busy) {
34597
34894
  idleSince = null;
34598
34895
  } else {
@@ -34849,7 +35146,7 @@ class GodotServer {
34849
35146
  async ownRunOnItsWay(projectPath, budgetMs) {
34850
35147
  const run = this.currentRun();
34851
35148
  const project = projectPath ?? run?.projectPath ?? null;
34852
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync10(join16(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
35149
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync11(join17(project, RUNTIME_AUTOLOAD.path)) || !await this.runStillGoing(run)) {
34853
35150
  return null;
34854
35151
  }
34855
35152
  const already = runtimesAnnounced().running;
@@ -34858,7 +35155,7 @@ class GodotServer {
34858
35155
  return !run.announcedBefore.has(one.pid);
34859
35156
  }
34860
35157
  try {
34861
- return statSync7(one.file).mtimeMs >= run.startedAt;
35158
+ return statSync8(one.file).mtimeMs >= run.startedAt;
34862
35159
  } catch {
34863
35160
  return false;
34864
35161
  }
@@ -34928,8 +35225,8 @@ class GodotServer {
34928
35225
  return this.createErrorResponse(refusal);
34929
35226
  }
34930
35227
  }
34931
- const screenshotDir = expectsScreenshot ? mkdtempSync2(join16(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
34932
- const screenshotPath = screenshotDir ? join16(screenshotDir, "capture.png") : null;
35228
+ const screenshotDir = expectsScreenshot ? mkdtempSync2(join17(tmpdir4(), "gdharness-runtime-screenshot-")) : null;
35229
+ const screenshotPath = screenshotDir ? join17(screenshotDir, "capture.png") : null;
34933
35230
  try {
34934
35231
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
34935
35232
  if (!reply.ok && reply.requestId !== undefined) {
@@ -34970,7 +35267,7 @@ class GodotServer {
34970
35267
  type: "text",
34971
35268
  text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
34972
35269
  },
34973
- { type: "image", data: readFileSync16(screenshotPath).toString("base64"), mimeType: "image/png" }
35270
+ { type: "image", data: readFileSync17(screenshotPath).toString("base64"), mimeType: "image/png" }
34974
35271
  ]
34975
35272
  };
34976
35273
  } finally {