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/cli.js CHANGED
@@ -388,6 +388,7 @@ function runArguments(options) {
388
388
  function editorArguments(projectPath, ports, logFile) {
389
389
  return [
390
390
  "-e",
391
+ "--headless",
391
392
  "--path",
392
393
  projectPath,
393
394
  "--lsp-port",
@@ -15874,7 +15875,7 @@ var init_tool_definitions = __esm(() => {
15874
15875
  },
15875
15876
  {
15876
15877
  name: "project_settings",
15877
- description: "Reads or writes project.godot: settings, autoloads, the main scene, input actions, plugins and audio buses.",
15878
+ 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.",
15878
15879
  parameters: {
15879
15880
  projectPath: PROJECT_PATH,
15880
15881
  setting: { type: "string", description: 'Setting path, such as "display/window/size/viewport_width".' },
@@ -15986,19 +15987,30 @@ var init_tool_definitions = __esm(() => {
15986
15987
  description: 'set_options: import options keyed as the .import file spells them, {"compress/mode": 1}.',
15987
15988
  additionalProperties: true
15988
15989
  },
15989
- reimport: { type: "boolean", description: "set_options: reimport afterwards. Default true." },
15990
- force: { type: "boolean", description: "reimport: reimport even what is current. Default false." }
15990
+ reimport: {
15991
+ type: "boolean",
15992
+ description: "set_options: reimport afterwards, answered under reimport as project_import reimport answers. Default true."
15993
+ },
15994
+ force: { type: "boolean", description: "reimport: reimport even what is current. Default false." },
15995
+ timeoutMs: {
15996
+ type: "integer",
15997
+ minimum: 1,
15998
+ description: "reimport, set_options: how long to wait for the editor to finish reimporting. Default 300000."
15999
+ }
15991
16000
  },
15992
16001
  requires: ["projectPath"],
15993
16002
  operations: {
15994
16003
  status: {
15995
- 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",
16004
+ 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",
15996
16005
  requires: []
15997
16006
  },
15998
16007
  options: { summary: "the import options of one resource", requires: ["resourcePath"] },
15999
- set_options: { summary: "change import options", requires: ["resourcePath", "options"] },
16008
+ set_options: {
16009
+ summary: "change import options in the sidecar, then apply them with a reimport of that resource",
16010
+ requires: ["resourcePath", "options"]
16011
+ },
16000
16012
  reimport: {
16001
- summary: "reimport one resource, or everything modified without resourcePath",
16013
+ 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",
16002
16014
  requires: []
16003
16015
  },
16004
16016
  uid: { summary: "the UID of one file", requires: ["resourcePath"] },
@@ -16300,7 +16312,7 @@ var init_tool_definitions = __esm(() => {
16300
16312
  },
16301
16313
  {
16302
16314
  name: "editor_launch",
16303
- 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.",
16315
+ 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.",
16304
16316
  parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
16305
16317
  requires: [],
16306
16318
  operations: {
@@ -16326,6 +16338,11 @@ var init_tool_definitions = __esm(() => {
16326
16338
  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.`
16327
16339
  },
16328
16340
  headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
16341
+ visible: {
16342
+ type: "boolean",
16343
+ ops: ["start"],
16344
+ 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."
16345
+ },
16329
16346
  savesIn: {
16330
16347
  type: "string",
16331
16348
  ops: ["start", "check"],
@@ -16656,7 +16673,7 @@ var init_tool_definitions = __esm(() => {
16656
16673
  },
16657
16674
  {
16658
16675
  name: "runtime_capture",
16659
- description: "A picture of the running game: the whole screen or one viewport, as an image. Needs the game running with a window.",
16676
+ 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.",
16660
16677
  parameters: {
16661
16678
  projectPath: RUNNING_PROJECT_PATH,
16662
16679
  pid: RUNNING_PID,
@@ -16923,7 +16940,6 @@ var init_headless_operations = __esm(() => {
16923
16940
  status: "get_import_status",
16924
16941
  options: "get_import_options",
16925
16942
  set_options: "set_import_options",
16926
- reimport: "reimport_resource",
16927
16943
  uid: "get_uid",
16928
16944
  refresh_classes: "refresh_class_cache"
16929
16945
  }),
@@ -16937,7 +16953,7 @@ var init_headless_operations = __esm(() => {
16937
16953
  })
16938
16954
  });
16939
16955
  ENGINE_PASSES = dictionary({
16940
- project_import: dictionary({ refresh_uids: "import" })
16956
+ project_import: dictionary({ refresh_uids: "import", reimport: "import" })
16941
16957
  });
16942
16958
  EDITOR_READS = dictionary({
16943
16959
  project_settings: dictionary({ get: "get_project_setting" })
@@ -33091,6 +33107,10 @@ var init_dap_client = __esm(() => {
33091
33107
  init_ports();
33092
33108
  });
33093
33109
 
33110
+ // src/desktop.ts
33111
+ var HIDDEN_DESKTOP = "gdharness";
33112
+ var init_desktop = () => {};
33113
+
33094
33114
  // src/editor-log.ts
33095
33115
  import {
33096
33116
  closeSync,
@@ -37786,7 +37806,8 @@ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = K
37786
37806
  command: spec.command,
37787
37807
  args: spec.args,
37788
37808
  envChanges: changesFrom(spec.env, process.env),
37789
- ...spec.run === undefined ? {} : { run: spec.run }
37809
+ ...spec.run === undefined ? {} : { run: spec.run },
37810
+ ...spec.desktop === undefined ? {} : { desktop: spec.desktop }
37790
37811
  });
37791
37812
  let printed = "";
37792
37813
  let complained = "";
@@ -37980,17 +38001,38 @@ var init_project_scan = __esm(() => {
37980
38001
  CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
37981
38002
  });
37982
38003
 
38004
+ // src/reimport.ts
38005
+ import { createHash as createHash5 } from "node:crypto";
38006
+ import { existsSync as existsSync15, readFileSync as readFileSync19, rmSync as rmSync10, statSync as statSync7, utimesSync, writeFileSync as writeFileSync14 } from "node:fs";
38007
+ import { basename as basename3, join as join21 } from "node:path";
38008
+ function forceNextImport(projectPath, resourcePath) {
38009
+ const relative = resourcePath.replace(/^res:\/\//, "");
38010
+ const sidecar = join21(projectPath, `${relative}.import`);
38011
+ if (!existsSync15(sidecar)) {
38012
+ return;
38013
+ }
38014
+ const { atime, mtimeMs } = statSync7(sidecar);
38015
+ writeFileSync14(sidecar, readFileSync19(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
38016
+ utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
38017
+ rmSync10(importRecord(projectPath, resourcePath), { force: true });
38018
+ }
38019
+ function importRecord(projectPath, resourcePath) {
38020
+ const digest = createHash5("md5").update(resourcePath).digest("hex");
38021
+ return join21(projectPath, ".godot", "imported", `${basename3(resourcePath)}-${digest}.md5`);
38022
+ }
38023
+ var init_reimport = () => {};
38024
+
37983
38025
  // src/restart-note.ts
37984
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, rmSync as rmSync10, writeFileSync as writeFileSync14 } from "node:fs";
37985
- import { dirname as dirname15, join as join21 } from "node:path";
38026
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync20, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
38027
+ import { dirname as dirname15, join as join22 } from "node:path";
37986
38028
  function restartNotePath(projectPath) {
37987
- return join21(projectPath, ".godot", "gdharness-restart.json");
38029
+ return join22(projectPath, ".godot", "gdharness-restart.json");
37988
38030
  }
37989
38031
  function noteRestartBegun(note) {
37990
38032
  const path = restartNotePath(note.projectPath);
37991
38033
  try {
37992
38034
  mkdirSync12(dirname15(path), { recursive: true });
37993
- writeFileSync14(path, `${JSON.stringify(note, null, 2)}
38035
+ writeFileSync15(path, `${JSON.stringify(note, null, 2)}
37994
38036
  `, "utf8");
37995
38037
  } catch (error) {
37996
38038
  console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
@@ -37998,7 +38040,7 @@ function noteRestartBegun(note) {
37998
38040
  }
37999
38041
  function restartOwed(projectPath) {
38000
38042
  try {
38001
- const parsed = JSON.parse(readFileSync19(restartNotePath(projectPath), "utf8"));
38043
+ const parsed = JSON.parse(readFileSync20(restartNotePath(projectPath), "utf8"));
38002
38044
  if (typeof parsed !== "object" || parsed === null) {
38003
38045
  return null;
38004
38046
  }
@@ -38013,7 +38055,7 @@ function restartOwed(projectPath) {
38013
38055
  }
38014
38056
  function restartSettled(projectPath) {
38015
38057
  try {
38016
- rmSync10(restartNotePath(projectPath), { force: true });
38058
+ rmSync11(restartNotePath(projectPath), { force: true });
38017
38059
  } catch (error) {
38018
38060
  console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
38019
38061
  }
@@ -38024,33 +38066,33 @@ var init_restart_note = __esm(() => {
38024
38066
 
38025
38067
  // src/run-record.ts
38026
38068
  import { execFileSync as execFileSync2 } from "node:child_process";
38027
- import { createHash as createHash5 } from "node:crypto";
38069
+ import { createHash as createHash6 } from "node:crypto";
38028
38070
  import {
38029
38071
  mkdirSync as mkdirSync13,
38030
38072
  openSync as openSync2,
38031
38073
  readdirSync as readdirSync8,
38032
- readFileSync as readFileSync20,
38074
+ readFileSync as readFileSync21,
38033
38075
  realpathSync as realpathSync3,
38034
- rmSync as rmSync11,
38035
- statSync as statSync7,
38036
- writeFileSync as writeFileSync15
38076
+ rmSync as rmSync12,
38077
+ statSync as statSync8,
38078
+ writeFileSync as writeFileSync16
38037
38079
  } from "node:fs";
38038
- import { basename as basename3, dirname as dirname16, join as join22, resolve as resolve5 } from "node:path";
38080
+ import { basename as basename4, dirname as dirname16, join as join23, resolve as resolve5 } from "node:path";
38039
38081
  function runsDirectory() {
38040
- return join22(runtimeDirectory(), "runs");
38082
+ return join23(runtimeDirectory(), "runs");
38041
38083
  }
38042
38084
  function realAsFarAsItExists(path) {
38043
38085
  const missing = [];
38044
38086
  let at = path;
38045
38087
  for (;; ) {
38046
38088
  try {
38047
- return join22(realpathSync3.native(at), ...missing);
38089
+ return join23(realpathSync3.native(at), ...missing);
38048
38090
  } catch {
38049
38091
  const parent = dirname16(at);
38050
38092
  if (parent === at) {
38051
38093
  return path;
38052
38094
  }
38053
- missing.unshift(basename3(at));
38095
+ missing.unshift(basename4(at));
38054
38096
  at = parent;
38055
38097
  }
38056
38098
  }
@@ -38058,26 +38100,26 @@ function realAsFarAsItExists(path) {
38058
38100
  function projectKey(projectPath) {
38059
38101
  const real = realAsFarAsItExists(resolve5(projectPath)).replace(/[\\/]+$/, "");
38060
38102
  const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
38061
- return createHash5("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
38103
+ return createHash6("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
38062
38104
  }
38063
38105
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
38064
- return join22(directory, "runs", `note-${projectKey(projectPath)}.json`);
38106
+ return join23(directory, "runs", `note-${projectKey(projectPath)}.json`);
38065
38107
  }
38066
38108
  function openTranscript(startedAt) {
38067
38109
  mkdirSync13(runsDirectory(), { recursive: true });
38068
- const path = join22(runsDirectory(), `run-${startedAt}.log`);
38110
+ const path = join23(runsDirectory(), `run-${startedAt}.log`);
38069
38111
  return { path, fd: openSync2(path, "a") };
38070
38112
  }
38071
38113
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
38072
- return join22(directory, "runs", `editor-${projectKey(projectPath)}.json`);
38114
+ return join23(directory, "runs", `editor-${projectKey(projectPath)}.json`);
38073
38115
  }
38074
38116
  function writeEditorRunNote(note) {
38075
38117
  mkdirSync13(runsDirectory(), { recursive: true });
38076
- writeFileSync15(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
38118
+ writeFileSync16(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
38077
38119
  }
38078
38120
  function readEditorRunNote(projectPath) {
38079
38121
  for (const directory of runtimeDirectories()) {
38080
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join22(directory, "runs", "editor-run.json"));
38122
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join23(directory, "runs", "editor-run.json"));
38081
38123
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
38082
38124
  return note;
38083
38125
  }
@@ -38087,7 +38129,7 @@ function readEditorRunNote(projectPath) {
38087
38129
  function editorNoteAt(path) {
38088
38130
  let parsed;
38089
38131
  try {
38090
- parsed = JSON.parse(readFileSync20(path, "utf8"));
38132
+ parsed = JSON.parse(readFileSync21(path, "utf8"));
38091
38133
  } catch {
38092
38134
  return null;
38093
38135
  }
@@ -38109,7 +38151,7 @@ function readRunRecord(projectPath) {
38109
38151
  if (own !== null) {
38110
38152
  return own;
38111
38153
  }
38112
- const shared = recordAt(join22(directory, "runs", SHARED_NOTE));
38154
+ const shared = recordAt(join23(directory, "runs", SHARED_NOTE));
38113
38155
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
38114
38156
  return shared;
38115
38157
  }
@@ -38121,13 +38163,13 @@ function everyRunRecord() {
38121
38163
  for (const directory of runtimeDirectories()) {
38122
38164
  let entries;
38123
38165
  try {
38124
- entries = readdirSync8(join22(directory, "runs"));
38166
+ entries = readdirSync8(join23(directory, "runs"));
38125
38167
  } catch {
38126
38168
  continue;
38127
38169
  }
38128
38170
  for (const entry of entries) {
38129
38171
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
38130
- const record = recordAt(join22(directory, "runs", entry));
38172
+ const record = recordAt(join23(directory, "runs", entry));
38131
38173
  if (record !== null) {
38132
38174
  found.push(record);
38133
38175
  }
@@ -38139,7 +38181,7 @@ function everyRunRecord() {
38139
38181
  function recordAt(path) {
38140
38182
  let parsed;
38141
38183
  try {
38142
- parsed = JSON.parse(readFileSync20(path, "utf8"));
38184
+ parsed = JSON.parse(readFileSync21(path, "utf8"));
38143
38185
  } catch {
38144
38186
  return null;
38145
38187
  }
@@ -38185,7 +38227,7 @@ function runningAs(pid) {
38185
38227
  const began = startedAt(pid, null);
38186
38228
  const when = began === null ? {} : { startedAt: began };
38187
38229
  if (process.platform === "linux") {
38188
- const raw = readFileSync20(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
38230
+ const raw = readFileSync21(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
38189
38231
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
38190
38232
  }
38191
38233
  const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
@@ -38287,7 +38329,7 @@ function judgeRun(record, running, needed) {
38287
38329
  if (running.startedAt !== undefined && running.startedAt - record.startedAt > SAME_RUN_WINDOW_MS) {
38288
38330
  return false;
38289
38331
  }
38290
- const engine = record.command === undefined ? null : basename3(record.command);
38332
+ const engine = record.command === undefined ? null : basename4(record.command);
38291
38333
  const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
38292
38334
  const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
38293
38335
  if (wanted !== null && !said.includes(wanted)) {
@@ -38323,19 +38365,19 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
38323
38365
  }
38324
38366
  function clearRunRecord(projectPath) {
38325
38367
  for (const directory of runtimeDirectories()) {
38326
- const shared = join22(directory, "runs", SHARED_NOTE);
38368
+ const shared = join23(directory, "runs", SHARED_NOTE);
38327
38369
  const sharedRecord = recordAt(shared);
38328
38370
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
38329
38371
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
38330
38372
  try {
38331
- rmSync11(path, { force: true });
38373
+ rmSync12(path, { force: true });
38332
38374
  } catch {}
38333
38375
  }
38334
38376
  }
38335
38377
  }
38336
38378
  function ageMs(path, now) {
38337
38379
  try {
38338
- return now - statSync7(path).mtimeMs;
38380
+ return now - statSync8(path).mtimeMs;
38339
38381
  } catch {
38340
38382
  return 0;
38341
38383
  }
@@ -38353,12 +38395,12 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
38353
38395
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
38354
38396
  continue;
38355
38397
  }
38356
- const path = join22(directory, entry);
38398
+ const path = join23(directory, entry);
38357
38399
  if (kept.has(path) || ageMs(path, now) < keepMs) {
38358
38400
  continue;
38359
38401
  }
38360
38402
  try {
38361
- rmSync11(path);
38403
+ rmSync12(path);
38362
38404
  } catch {}
38363
38405
  }
38364
38406
  }
@@ -38377,19 +38419,19 @@ import {
38377
38419
  closeSync as closeSync2,
38378
38420
  constants,
38379
38421
  copyFileSync,
38380
- existsSync as existsSync15,
38422
+ existsSync as existsSync16,
38381
38423
  fstatSync,
38382
38424
  mkdirSync as mkdirSync14,
38383
38425
  mkdtempSync as mkdtempSync3,
38384
38426
  openSync as openSync3,
38385
38427
  readdirSync as readdirSync9,
38386
- readFileSync as readFileSync21,
38428
+ readFileSync as readFileSync22,
38387
38429
  readSync as readSync2,
38388
38430
  rmdirSync,
38389
- statSync as statSync8
38431
+ statSync as statSync9
38390
38432
  } from "node:fs";
38391
38433
  import { tmpdir as tmpdir5 } from "node:os";
38392
- import { basename as basename4, dirname as dirname17, extname as extname2, isAbsolute as isAbsolute3, join as join23, normalize as normalize3, relative as relative3 } from "node:path";
38434
+ import { basename as basename5, dirname as dirname17, extname as extname2, isAbsolute as isAbsolute3, join as join24, normalize as normalize3, relative as relative3 } from "node:path";
38393
38435
  import { setTimeout as delay3 } from "node:timers/promises";
38394
38436
  import { fileURLToPath as fileURLToPath6 } from "node:url";
38395
38437
  import { promisify as promisify7 } from "node:util";
@@ -38404,15 +38446,15 @@ function captureDestinationRefusal(outputPath, projects) {
38404
38446
  return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
38405
38447
  }
38406
38448
  const directory = realPathOr(dirname17(outputPath));
38407
- if (!existsSync15(directory)) {
38449
+ if (!existsSync16(directory)) {
38408
38450
  return `outputPath's directory ${dirname17(outputPath)} does not exist; a capture creates no directories.`;
38409
38451
  }
38410
- const where = join23(directory, basename4(outputPath));
38452
+ const where = join24(directory, basename5(outputPath));
38411
38453
  const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
38412
38454
  if (inside !== undefined) {
38413
38455
  return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
38414
38456
  }
38415
- if (existsSync15(where)) {
38457
+ if (existsSync16(where)) {
38416
38458
  return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
38417
38459
  }
38418
38460
  return null;
@@ -38769,6 +38811,28 @@ function scanWaitAnswer(scanned) {
38769
38811
  scanNote: scanned.stillScanning ? `The editor was still scanning the project after ${SCAN_WAIT_MS / 1000} seconds, so this ` + "game started during the scan and may have read a class cache being rewritten. If " + 'editor_output shows "Could not find type" for a class that is declared, editor_rescan ' + "waits for the scan to finish, and a start after it reads the finished cache." : undefined
38770
38812
  };
38771
38813
  }
38814
+ function changedSettings(before, after) {
38815
+ if (before === null || after === null) {
38816
+ return null;
38817
+ }
38818
+ const names = new Set([...before.keys(), ...after.keys()]);
38819
+ return [...names].filter((name) => JSON.stringify(before.get(name)) !== JSON.stringify(after.get(name))).sort();
38820
+ }
38821
+ function importStatuses(payload) {
38822
+ const listed = payload["resources"];
38823
+ if (!Array.isArray(listed)) {
38824
+ return [];
38825
+ }
38826
+ return listed.map((entry) => {
38827
+ const one = asParams(entry);
38828
+ const reason = readString(one, "reason");
38829
+ return {
38830
+ path: readString(one, "path") ?? "",
38831
+ status: readString(one, "status") ?? "unknown",
38832
+ ...reason === undefined ? {} : { reason }
38833
+ };
38834
+ });
38835
+ }
38772
38836
  function escapingPropertyValue(properties) {
38773
38837
  if (typeof properties !== "object" || properties === null) {
38774
38838
  return;
@@ -38799,7 +38863,7 @@ function aboveTheRunner(entry) {
38799
38863
  }
38800
38864
  function elsewhereIn(projectPath, asked) {
38801
38865
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
38802
- if (wanted === "" || existsSync15(join23(projectPath, wanted))) {
38866
+ if (wanted === "" || existsSync16(join24(projectPath, wanted))) {
38803
38867
  return "";
38804
38868
  }
38805
38869
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -38813,7 +38877,7 @@ function elsewhereIn(projectPath, asked) {
38813
38877
  return near.length === 0 ? "" : ` The project does have ${there}.`;
38814
38878
  }
38815
38879
  function hasMainScene(projectFile) {
38816
- const scene = parseProjectGodot(readFileSync21(projectFile, "utf8"))["application"]?.["run/main_scene"];
38880
+ const scene = parseProjectGodot(readFileSync22(projectFile, "utf8"))["application"]?.["run/main_scene"];
38817
38881
  return typeof scene === "string" && scene !== "";
38818
38882
  }
38819
38883
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -38821,13 +38885,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
38821
38885
  return false;
38822
38886
  }
38823
38887
  try {
38824
- return readFileSync21(join23(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
38888
+ return readFileSync22(join24(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
38825
38889
  } catch {
38826
38890
  return false;
38827
38891
  }
38828
38892
  }
38829
38893
  function editorPlaysHeadless(projectFile) {
38830
- const runArgs = parseProjectGodot(readFileSync21(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
38894
+ const runArgs = parseProjectGodot(readFileSync22(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
38831
38895
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
38832
38896
  }
38833
38897
  function camelCased(params) {
@@ -38846,7 +38910,7 @@ function needsItsOwnProcess(asked) {
38846
38910
  class GodotServer {
38847
38911
  mcp;
38848
38912
  locator = new GodotLocator2;
38849
- operationsScript = join23(__dirname2, "godot", "operations", "godot_operations.gd");
38913
+ operationsScript = join24(__dirname2, "godot", "operations", "godot_operations.gd");
38850
38914
  godotBridge;
38851
38915
  tools = buildToolDefinitions();
38852
38916
  activeProcess = null;
@@ -39291,7 +39355,14 @@ class GodotServer {
39291
39355
  }
39292
39356
  async dispatch(tool, op, args) {
39293
39357
  if (ENGINE_PASSES[tool]?.[op] !== undefined) {
39294
- return await this.handleRefreshUids(args);
39358
+ return op === "reimport" ? await this.handleReimport(args) : await this.handleRefreshUids(args);
39359
+ }
39360
+ const settingsWrite = tool === "project_settings" && op !== "get" ? HEADLESS_OPERATIONS[tool]?.[op] : undefined;
39361
+ if (settingsWrite !== undefined) {
39362
+ return await this.handleProjectSettingsWrite(op, settingsWrite, args);
39363
+ }
39364
+ if (tool === "project_import" && op === "set_options") {
39365
+ return await this.handleSetImportOptions(args);
39295
39366
  }
39296
39367
  const headless = HEADLESS_OPERATIONS[tool]?.[op];
39297
39368
  if (headless !== undefined) {
@@ -39550,8 +39621,8 @@ class GodotServer {
39550
39621
  if (path === undefined) {
39551
39622
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
39552
39623
  }
39553
- const file = join23(path, "project.godot");
39554
- if (!existsSync15(file)) {
39624
+ const file = join24(path, "project.godot");
39625
+ if (!existsSync16(file)) {
39555
39626
  return {
39556
39627
  ok: false,
39557
39628
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -39822,6 +39893,222 @@ class GodotServer {
39822
39893
  }
39823
39894
  }, scanned));
39824
39895
  }
39896
+ async handleReimport(args) {
39897
+ const project = this.project(args);
39898
+ if (!project.ok) {
39899
+ return project.response;
39900
+ }
39901
+ const contained = this.containProjectFiles(args);
39902
+ if (!contained.ok) {
39903
+ return contained.response;
39904
+ }
39905
+ const outcome = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), args["force"] === true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
39906
+ return outcome.ok ? this.jsonTextResponse(outcome.payload) : outcome.response;
39907
+ }
39908
+ editorHasOpen(projectPath) {
39909
+ const open = this.godotBridge.getStatus().projectPath;
39910
+ return this.godotBridge.isConnected() && open !== undefined && open !== "" && isSameDirectory(open, projectPath);
39911
+ }
39912
+ async handleProjectSettingsWrite(op, operation, args) {
39913
+ const project = this.project(args);
39914
+ if (!project.ok) {
39915
+ return project.response;
39916
+ }
39917
+ const projectPath = project.value.path;
39918
+ const served = this.editorHasOpen(projectPath);
39919
+ const before = served ? this.settingKeysOf(projectPath) : null;
39920
+ const written = await this.headless(operation, args);
39921
+ if (!served || written.isError === true) {
39922
+ return written;
39923
+ }
39924
+ const answer = asParams(JSON.parse(written.content[0]?.text ?? "{}"));
39925
+ if (AUDIO_BUS_OPS.has(op)) {
39926
+ return this.jsonTextResponse({ ...answer, ...await this.editorTakes("adopt_audio_bus_layout", {}) });
39927
+ }
39928
+ const changed = changedSettings(before, this.settingKeysOf(projectPath));
39929
+ if (changed === null) {
39930
+ return this.jsonTextResponse({
39931
+ ...answer,
39932
+ 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."
39933
+ });
39934
+ }
39935
+ if (changed.length === 0) {
39936
+ return written;
39937
+ }
39938
+ const taken = await this.editorTakes("adopt_project_settings", { settings: changed });
39939
+ return this.jsonTextResponse({
39940
+ ...answer,
39941
+ ...taken,
39942
+ ...taken["editorNote"] === undefined ? { editorAdopted: changed } : {}
39943
+ });
39944
+ }
39945
+ async editorTakes(command, args) {
39946
+ try {
39947
+ await this.godotBridge.invokeTool(command, args);
39948
+ return {};
39949
+ } catch (error) {
39950
+ const message = errorMessage(error);
39951
+ const why = message.includes("Unknown tool") ? "runs an addon from before writes were handed to it" : `could not take the change (${message})`;
39952
+ return {
39953
+ 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.`
39954
+ };
39955
+ }
39956
+ }
39957
+ async handleSetImportOptions(args) {
39958
+ const project = this.project(args);
39959
+ if (!project.ok) {
39960
+ return project.response;
39961
+ }
39962
+ const contained = this.containProjectFiles(args);
39963
+ if (!contained.ok) {
39964
+ return contained.response;
39965
+ }
39966
+ const {
39967
+ op: _op,
39968
+ projectPath: _projectPath,
39969
+ reimport: _reimport,
39970
+ timeoutMs: _timeoutMs,
39971
+ ...params
39972
+ } = contained.value;
39973
+ const written = await this.operation("set_import_options", params, project.value.path);
39974
+ if (!written.ok || args["reimport"] === false) {
39975
+ return this.answer(written.ok ? {
39976
+ ...written,
39977
+ payload: {
39978
+ ...written.payload,
39979
+ note: "Not reimported: the editor imports it with these options on its next scan, or project_import reimport does now."
39980
+ }
39981
+ } : written);
39982
+ }
39983
+ const applied = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
39984
+ if (!applied.ok) {
39985
+ return applied.response;
39986
+ }
39987
+ return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
39988
+ }
39989
+ async reimport(projectPath, resourcePath, force, timeoutMs) {
39990
+ const ask = resourcePath === undefined ? { includeUpToDate: force } : { resourcePath };
39991
+ const before = await this.operation("get_import_status", ask, projectPath);
39992
+ if (!before.ok) {
39993
+ return { ok: false, response: this.answer(before) };
39994
+ }
39995
+ const listed = importStatuses(before.payload);
39996
+ const missingSource = listed.filter((one) => one.status === "missing_source").map((one) => one.path);
39997
+ if (resourcePath !== undefined && missingSource.length > 0) {
39998
+ return {
39999
+ ok: false,
40000
+ response: this.createErrorResponse(`${missingSource[0]} is not on disk, so there is nothing to import.`)
40001
+ };
40002
+ }
40003
+ const targets = listed.filter((one) => one.status !== "missing_source" && (force || one.status !== "up_to_date")).map((one) => one.path);
40004
+ if (targets.length === 0) {
40005
+ return {
40006
+ ok: true,
40007
+ payload: {
40008
+ reimported: [],
40009
+ notReimported: [],
40010
+ 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.`
40011
+ }
40012
+ };
40013
+ }
40014
+ const viaEditor = this.editorHasOpen(projectPath);
40015
+ const extra = {};
40016
+ if (viaEditor) {
40017
+ const inEditor = await this.reimportInTheEditor(targets, timeoutMs);
40018
+ if (!inEditor.ok) {
40019
+ return inEditor;
40020
+ }
40021
+ if (!inEditor.finished) {
40022
+ return {
40023
+ ok: true,
40024
+ payload: {
40025
+ via: "editor",
40026
+ stillImporting: true,
40027
+ asked: targets,
40028
+ 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.`
40029
+ }
40030
+ };
40031
+ }
40032
+ } else {
40033
+ const engine = await this.engine();
40034
+ if (!engine.ok) {
40035
+ return { ok: false, response: engine.response };
40036
+ }
40037
+ for (const target of targets) {
40038
+ forceNextImport(projectPath, target);
40039
+ }
40040
+ const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
40041
+ const imported = await runImport(engine.value, projectPath);
40042
+ if (!imported.ok) {
40043
+ return { ok: false, response: this.answer(withScanWait(imported, scanned)) };
40044
+ }
40045
+ if (imported.messages.length > 0) {
40046
+ extra["engine_messages"] = imported.messages;
40047
+ }
40048
+ }
40049
+ const after = await this.operation("get_import_status", resourcePath === undefined ? { includeUpToDate: true } : { resourcePath }, projectPath);
40050
+ if (!after.ok) {
40051
+ return { ok: false, response: this.answer(after) };
40052
+ }
40053
+ const now = new Map(importStatuses(after.payload).map((one) => [one.path, one]));
40054
+ const reimported = targets.filter((path) => now.get(path)?.status === "up_to_date");
40055
+ const notReimported = targets.filter((path) => now.get(path)?.status !== "up_to_date").map((path) => ({
40056
+ path,
40057
+ status: now.get(path)?.status ?? "unknown",
40058
+ ...now.get(path)?.reason === undefined ? {} : { reason: now.get(path)?.reason }
40059
+ }));
40060
+ return {
40061
+ ok: true,
40062
+ payload: {
40063
+ via: viaEditor ? "editor" : "engine",
40064
+ reimported,
40065
+ notReimported,
40066
+ ...missingSource.length > 0 ? { missingSource } : {},
40067
+ ...extra
40068
+ }
40069
+ };
40070
+ }
40071
+ async reimportInTheEditor(paths, timeoutMs) {
40072
+ const started = Date.now();
40073
+ let completedBefore;
40074
+ for (;; ) {
40075
+ while (Date.now() - started < timeoutMs && await this.editorIsScanningOrImporting()) {
40076
+ await new Promise((settle) => setTimeout(settle, 100));
40077
+ }
40078
+ let answer;
40079
+ try {
40080
+ answer = asParams(await this.godotBridge.invokeTool("reimport_files", { paths: [...paths] }));
40081
+ } catch (error) {
40082
+ const message = errorMessage(error);
40083
+ return {
40084
+ ok: false,
40085
+ response: message.includes("Unknown tool") ? this.createErrorResponse("The editor is running an addon from before reimport_files, so it cannot be asked to reimport.", [
40086
+ "editor_launch restart loads the addon this server ships",
40087
+ "Or close the editor, and project_import reimport runs the engine import instead"
40088
+ ]) : this.createErrorResponse(`The editor answered reimport_files with an error: ${message}`)
40089
+ };
40090
+ }
40091
+ if (answer["started"] !== false) {
40092
+ completedBefore = readNumber(answer, "reimportsCompletedBefore");
40093
+ break;
40094
+ }
40095
+ if (Date.now() - started >= timeoutMs) {
40096
+ return { ok: true, finished: false };
40097
+ }
40098
+ await new Promise((settle) => setTimeout(settle, 100));
40099
+ }
40100
+ while (Date.now() - started < timeoutMs) {
40101
+ await new Promise((settle) => setTimeout(settle, 250));
40102
+ try {
40103
+ const status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
40104
+ const completed = readNumber(status, "reimportsCompleted");
40105
+ if (completedBefore !== undefined && completed !== undefined && completed > completedBefore) {
40106
+ return { ok: true, finished: true };
40107
+ }
40108
+ } catch {}
40109
+ }
40110
+ return { ok: true, finished: false };
40111
+ }
39825
40112
  async operation(operation, params, projectPath) {
39826
40113
  const engine = await this.engine();
39827
40114
  if (!engine.ok) {
@@ -39863,11 +40150,11 @@ class GodotServer {
39863
40150
  if (!engine.ok) {
39864
40151
  return engine.response;
39865
40152
  }
39866
- const application = parseProjectGodot(readFileSync21(project.value.file, "utf8"))["application"] ?? {};
40153
+ const application = parseProjectGodot(readFileSync22(project.value.file, "utf8"))["application"] ?? {};
39867
40154
  const name = application["config/name"];
39868
40155
  const mainScene = application["run/main_scene"];
39869
40156
  const info = {
39870
- name: typeof name === "string" && name !== "" ? name : basename4(project.value.path),
40157
+ name: typeof name === "string" && name !== "" ? name : basename5(project.value.path),
39871
40158
  path: project.value.path,
39872
40159
  mainScene: typeof mainScene === "string" && mainScene !== "" ? mainScene : null,
39873
40160
  godotVersion: await this.godotVersion(engine.value),
@@ -39931,11 +40218,11 @@ class GodotServer {
39931
40218
  return this.createErrorResponse(`Cannot create ${dirname17(output.relativePath)} to export into: ${errorMessage(error)}`);
39932
40219
  }
39933
40220
  const debug = readBoolean(args, "debug") ?? false;
39934
- const exportLogs = mkdtempSync3(join23(tmpdir5(), "gdharness-export-"));
40221
+ const exportLogs = mkdtempSync3(join24(tmpdir5(), "gdharness-export-"));
39935
40222
  const exportArgs = [
39936
40223
  "--headless",
39937
40224
  "--log-file",
39938
- join23(exportLogs, "engine.log"),
40225
+ join24(exportLogs, "engine.log"),
39939
40226
  "--path",
39940
40227
  project.value.path,
39941
40228
  debug ? "--export-debug" : "--export-release",
@@ -39967,7 +40254,7 @@ class GodotServer {
39967
40254
  log.finish();
39968
40255
  const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
39969
40256
  const verdict = {
39970
- exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync15(output.absolutePath),
40257
+ exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync16(output.absolutePath),
39971
40258
  preset,
39972
40259
  outputPath: output.relativePath,
39973
40260
  debug,
@@ -40003,7 +40290,7 @@ class GodotServer {
40003
40290
  return contained.response;
40004
40291
  }
40005
40292
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
40006
- if (!existsSync15(join23(project.value.path, runner))) {
40293
+ if (!existsSync16(join24(project.value.path, runner))) {
40007
40294
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
40008
40295
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
40009
40296
  ]);
@@ -40039,7 +40326,7 @@ class GodotServer {
40039
40326
  ];
40040
40327
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
40041
40328
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
40042
- const userData = mkdtempSync3(join23(tmpdir5(), "gdharness-tests-"));
40329
+ const userData = mkdtempSync3(join24(tmpdir5(), "gdharness-tests-"));
40043
40330
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
40044
40331
  const console2 = [];
40045
40332
  run.process.stdout?.on("data", (data) => {
@@ -40059,14 +40346,14 @@ class GodotServer {
40059
40346
  resolve(false);
40060
40347
  });
40061
40348
  });
40062
- const reportsDir = join23(project.value.path, ".godot", ...ours.split("/"));
40349
+ const reportsDir = join24(project.value.path, ".godot", ...ours.split("/"));
40063
40350
  let report = null;
40064
40351
  let reportProblem = null;
40065
40352
  try {
40066
- const written = existsSync15(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
40353
+ const written = existsSync16(reportsDir) ? readdirSync9(reportsDir).filter((name) => name.startsWith("report_")).sort((a, b) => Number(a.slice("report_".length)) - Number(b.slice("report_".length))) : [];
40067
40354
  const newest = written.at(-1);
40068
40355
  if (newest !== undefined) {
40069
- report = parseJUnit(readFileSync21(join23(reportsDir, newest, "results.xml"), "utf8"));
40356
+ report = parseJUnit(readFileSync22(join24(reportsDir, newest, "results.xml"), "utf8"));
40070
40357
  }
40071
40358
  } catch (error) {
40072
40359
  reportProblem = errorMessage(error);
@@ -40209,11 +40496,11 @@ class GodotServer {
40209
40496
  const cached = cachedClasses(projectPath);
40210
40497
  const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
40211
40498
  const contained = resolveWithinProject(projectPath, resourcePath);
40212
- if (!contained.ok || !existsSync15(contained.absolutePath)) {
40499
+ if (!contained.ok || !existsSync16(contained.absolutePath)) {
40213
40500
  return null;
40214
40501
  }
40215
40502
  try {
40216
- return readFileSync21(contained.absolutePath, "utf8");
40503
+ return readFileSync22(contained.absolutePath, "utf8");
40217
40504
  } catch {
40218
40505
  return null;
40219
40506
  }
@@ -40263,11 +40550,11 @@ class GodotServer {
40263
40550
  }
40264
40551
  sourceOfClass(projectPath, resourcePath) {
40265
40552
  const contained = resolveWithinProject(projectPath, resourcePath);
40266
- if (!contained.ok || !existsSync15(contained.absolutePath)) {
40553
+ if (!contained.ok || !existsSync16(contained.absolutePath)) {
40267
40554
  return null;
40268
40555
  }
40269
40556
  try {
40270
- return readFileSync21(contained.absolutePath, "utf8");
40557
+ return readFileSync22(contained.absolutePath, "utf8");
40271
40558
  } catch {
40272
40559
  return null;
40273
40560
  }
@@ -40536,7 +40823,7 @@ class GodotServer {
40536
40823
  return null;
40537
40824
  }
40538
40825
  try {
40539
- return settingKeys(readFileSync21(join23(projectPath, "project.godot"), "utf8"));
40826
+ return settingKeys(readFileSync22(join24(projectPath, "project.godot"), "utf8"));
40540
40827
  } catch {
40541
40828
  return null;
40542
40829
  }
@@ -40676,7 +40963,8 @@ class GodotServer {
40676
40963
  GDHARNESS_LSP_PORT: String(ports.lsp),
40677
40964
  GDHARNESS_DAP_PORT: String(ports.dap),
40678
40965
  [OPENED_BY_A_SERVER]: "1"
40679
- }
40966
+ },
40967
+ desktop: HIDDEN_DESKTOP
40680
40968
  });
40681
40969
  if ("error" in launched) {
40682
40970
  return { pid: null, error: launched.error };
@@ -40758,7 +41046,7 @@ class GodotServer {
40758
41046
  userArgs: given.value
40759
41047
  });
40760
41048
  this.logDebug(`Running Godot project: ${engine.value} ${cmdArgs.join(" ")}`);
40761
- const started = await this.launchKeptGame(engine.value, cmdArgs, project.value.path, needsItsOwnProcess(asked.value) ? environmentFor({ ...asked.value, runtimeDirectory: runtimeDirectory() }) : undefined);
41049
+ 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);
40762
41050
  if ("error" in started) {
40763
41051
  return this.createErrorResponse(`The game could not be started: ${started.error}`, [
40764
41052
  "editor_status says which Godot this server runs and whether it answers"
@@ -40788,7 +41076,7 @@ class GodotServer {
40788
41076
  }
40789
41077
  async runtimeUp(projectPath, before, budget, withArgs) {
40790
41078
  const budgetMs = budget.ms;
40791
- if (!existsSync15(join23(projectPath, RUNTIME_AUTOLOAD2.path))) {
41079
+ if (!existsSync16(join24(projectPath, RUNTIME_AUTOLOAD2.path))) {
40792
41080
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
40793
41081
  }
40794
41082
  let asking = null;
@@ -40851,7 +41139,7 @@ class GodotServer {
40851
41139
  }
40852
41140
  let announcedAt;
40853
41141
  try {
40854
- announcedAt = statSync8(endpoint.file).mtimeMs;
41142
+ announcedAt = statSync9(endpoint.file).mtimeMs;
40855
41143
  } catch {
40856
41144
  return;
40857
41145
  }
@@ -41098,7 +41386,7 @@ class GodotServer {
41098
41386
  });
41099
41387
  return started;
41100
41388
  }
41101
- async launchKeptGame(godotPath, cmdArgs, projectPath, env) {
41389
+ async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop) {
41102
41390
  const startedAt = Date.now();
41103
41391
  const transcript = openTranscript(startedAt);
41104
41392
  closeSync2(transcript.fd);
@@ -41106,7 +41394,8 @@ class GodotServer {
41106
41394
  command: godotPath,
41107
41395
  args: cmdArgs,
41108
41396
  ...env === undefined ? {} : { env },
41109
- run: { transcript: transcript.path, startedAt, projectPath }
41397
+ run: { transcript: transcript.path, startedAt, projectPath },
41398
+ ...desktop === undefined ? {} : { desktop }
41110
41399
  });
41111
41400
  if ("error" in launched) {
41112
41401
  return launched;
@@ -41601,7 +41890,7 @@ class GodotServer {
41601
41890
  notes.push(endedWithoutACode({
41602
41891
  throughEditor: run.throughEditor,
41603
41892
  announced: announced !== undefined || run.announcedPid !== undefined,
41604
- couldAnnounce: run.projectPath !== null && existsSync15(join23(run.projectPath, RUNTIME_AUTOLOAD2.path)),
41893
+ couldAnnounce: run.projectPath !== null && existsSync16(join24(run.projectPath, RUNTIME_AUTOLOAD2.path)),
41605
41894
  endedHere: typeof run.endedHere === "string",
41606
41895
  pickedUp: run.pickedUp === true
41607
41896
  }));
@@ -41827,6 +42116,7 @@ class GodotServer {
41827
42116
  const ghosts = blind.stillHeld ?? [];
41828
42117
  let waitedForEditorMs = 0;
41829
42118
  let first;
42119
+ let completedBefore;
41830
42120
  for (;; ) {
41831
42121
  const busySince = Date.now();
41832
42122
  let wasBusy = false;
@@ -41841,8 +42131,9 @@ class GodotServer {
41841
42131
  if (first.isError) {
41842
42132
  return first;
41843
42133
  }
41844
- const declined = asParams(JSON.parse(first.content[0]?.text ?? "{}"))["started"] === false;
41845
- if (!declined || Date.now() - started >= timeoutMs) {
42134
+ const firstAnswer = asParams(JSON.parse(first.content[0]?.text ?? "{}"));
42135
+ completedBefore = readNumber(firstAnswer, "scansCompletedBefore");
42136
+ if (firstAnswer["started"] !== false || Date.now() - started >= timeoutMs) {
41846
42137
  break;
41847
42138
  }
41848
42139
  await new Promise((settle) => setTimeout(settle, 100));
@@ -41853,7 +42144,9 @@ class GodotServer {
41853
42144
  await new Promise((settle) => setTimeout(settle, 100));
41854
42145
  const status = asParams(await this.godotBridge.invokeTool("rescan_filesystem", { ...args, statusOnly: true }));
41855
42146
  const pending = Boolean(status["pending"]) && Date.now() - started < SCAN_START_MS;
41856
- busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending;
42147
+ const completed = readNumber(status, "scansCompleted");
42148
+ const notYetOver = completedBefore !== undefined && completed !== undefined && completed <= completedBefore;
42149
+ busy = Boolean(status["scanning"]) || Boolean(status["importing"]) || pending || notYetOver;
41857
42150
  if (busy) {
41858
42151
  idleSince = null;
41859
42152
  } else {
@@ -42110,7 +42403,7 @@ class GodotServer {
42110
42403
  async ownRunOnItsWay(projectPath, budgetMs) {
42111
42404
  const run = this.currentRun();
42112
42405
  const project = projectPath ?? run?.projectPath ?? null;
42113
- if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync15(join23(project, RUNTIME_AUTOLOAD2.path)) || !await this.runStillGoing(run)) {
42406
+ if (run === null || project === null || run.announcedPid !== undefined || run.projectPath !== null && !isSameDirectory(run.projectPath, project) || !existsSync16(join24(project, RUNTIME_AUTOLOAD2.path)) || !await this.runStillGoing(run)) {
42114
42407
  return null;
42115
42408
  }
42116
42409
  const already = runtimesAnnounced().running;
@@ -42119,7 +42412,7 @@ class GodotServer {
42119
42412
  return !run.announcedBefore.has(one.pid);
42120
42413
  }
42121
42414
  try {
42122
- return statSync8(one.file).mtimeMs >= run.startedAt;
42415
+ return statSync9(one.file).mtimeMs >= run.startedAt;
42123
42416
  } catch {
42124
42417
  return false;
42125
42418
  }
@@ -42189,8 +42482,8 @@ class GodotServer {
42189
42482
  return this.createErrorResponse(refusal);
42190
42483
  }
42191
42484
  }
42192
- const screenshotDir = expectsScreenshot ? mkdtempSync3(join23(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
42193
- const screenshotPath = screenshotDir ? join23(screenshotDir, "capture.png") : null;
42485
+ const screenshotDir = expectsScreenshot ? mkdtempSync3(join24(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
42486
+ const screenshotPath = screenshotDir ? join24(screenshotDir, "capture.png") : null;
42194
42487
  try {
42195
42488
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
42196
42489
  if (!reply.ok && reply.requestId !== undefined) {
@@ -42231,7 +42524,7 @@ class GodotServer {
42231
42524
  type: "text",
42232
42525
  text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
42233
42526
  },
42234
- { type: "image", data: readFileSync21(screenshotPath).toString("base64"), mimeType: "image/png" }
42527
+ { type: "image", data: readFileSync22(screenshotPath).toString("base64"), mimeType: "image/png" }
42235
42528
  ]
42236
42529
  };
42237
42530
  } finally {
@@ -42331,7 +42624,7 @@ async function runGodotServer() {
42331
42624
  const server = new GodotServer;
42332
42625
  await server.run();
42333
42626
  }
42334
- var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
42627
+ var UPDATE_NOTICE_EVERY = 500, FEEDBACK_NOTICE_EVERY = 250, run7, __dirname2, EDITOR_RESTART_TIMEOUT_MS = 90000, HOLD_PING_MS = 2000, SLOWEST_FRAME_RATE = 20, PLAY_STARTS_WITHIN_MS = 30000, STOP_WAIT_MS = 1e4, EXIT_REPORTED_WITHIN_MS = 2000, CONSOLE_WRAPPER, LONGEST_BOOT_NOTED_MS, MOST_GROUPS = 20, VALUE_NAMES, BRIDGE_RETRY_MS = 2000, SUCCESSOR_CHECK_MS = 1e4, HANDOVER_MS, HANDOVER_POLL_MS = 250, CACHE_WRITE_MS = 2000, SCAN_WAIT_MS = 30000, PLAYING_STATUS_MS = 1000, SCAN_START_MS = 2000, SCAN_SETTLED_MS = 500, REIMPORT_TIMEOUT_MS = 300000, AUDIO_BUS_OPS, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
42335
42628
  var init_server3 = __esm(() => {
42336
42629
  init_mcp();
42337
42630
  init_stdio2();
@@ -42344,6 +42637,7 @@ var init_server3 = __esm(() => {
42344
42637
  init_class_note();
42345
42638
  init_config_pin();
42346
42639
  init_dap_client();
42640
+ init_desktop();
42347
42641
  init_editor_log();
42348
42642
  init_errors();
42349
42643
  init_game_log();
@@ -42360,6 +42654,7 @@ var init_server3 = __esm(() => {
42360
42654
  init_process_children();
42361
42655
  init_process_time();
42362
42656
  init_project_scan();
42657
+ init_reimport();
42363
42658
  init_resources();
42364
42659
  init_restart_note();
42365
42660
  init_run_record();
@@ -42380,6 +42675,11 @@ var init_server3 = __esm(() => {
42380
42675
  object: "an object"
42381
42676
  };
42382
42677
  HANDOVER_MS = SUCCESSOR_CHECK_MS + 5000;
42678
+ AUDIO_BUS_OPS = new Set([
42679
+ "add_audio_bus",
42680
+ "set_audio_bus_effect",
42681
+ "set_audio_bus_volume"
42682
+ ]);
42383
42683
  PATH_SOLUTIONS = [
42384
42684
  'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
42385
42685
  "Point projectPath at the project the file belongs to"
@@ -42411,8 +42711,8 @@ var init_server3 = __esm(() => {
42411
42711
  });
42412
42712
 
42413
42713
  // src/cli.ts
42414
- import { existsSync as existsSync16 } from "fs";
42415
- import { join as join24, resolve as resolve6 } from "path";
42714
+ import { existsSync as existsSync17 } from "fs";
42715
+ import { join as join25, resolve as resolve6 } from "path";
42416
42716
 
42417
42717
  // src/bridge-announce.ts
42418
42718
  init_errors();
@@ -44236,7 +44536,7 @@ class UsageError extends Refusal2 {
44236
44536
  function projectArgument(at) {
44237
44537
  const named = args.slice(at).find((value) => !value.startsWith("--"));
44238
44538
  const projectPath = resolve6(named ?? ".");
44239
- if (!existsSync16(join24(projectPath, "project.godot"))) {
44539
+ if (!existsSync17(join25(projectPath, "project.godot"))) {
44240
44540
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
44241
44541
  }
44242
44542
  return projectPath;
@@ -44275,7 +44575,7 @@ async function registerRuntime(godot, projectPath) {
44275
44575
  return;
44276
44576
  }
44277
44577
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
44278
- if (!existsSync16(join24(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
44578
+ if (!existsSync17(join25(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
44279
44579
  console.log(` that file is not in this project, so it boots with a missing script: restore it, or point the entry at res://${RUNTIME_AUTOLOAD.path}`);
44280
44580
  }
44281
44581
  }
@@ -44407,7 +44707,7 @@ async function setup() {
44407
44707
  console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
44408
44708
  }
44409
44709
  if (!args.includes("--no-skill")) {
44410
- const directories = skillDirectories(chosen.harnesses, projectPath, existsSync16);
44710
+ const directories = skillDirectories(chosen.harnesses, projectPath, existsSync17);
44411
44711
  for (const written of writeSkill(directories, getLocalVersion())) {
44412
44712
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
44413
44713
  }
@@ -44501,7 +44801,7 @@ async function upgrade() {
44501
44801
  moved.push(took.from);
44502
44802
  }
44503
44803
  }
44504
- for (const written of writeSkill(skillDirectories(already, projectPath, existsSync16), version)) {
44804
+ for (const written of writeSkill(skillDirectories(already, projectPath, existsSync17), version)) {
44505
44805
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
44506
44806
  }
44507
44807
  console.log(`