gdharness 1.1.5 → 1.1.7

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
@@ -385,9 +385,10 @@ function runArguments(options) {
385
385
  }
386
386
  return args;
387
387
  }
388
- function editorArguments(projectPath, ports, logFile) {
388
+ function editorArguments(projectPath, ports, logFile, hidden) {
389
389
  return [
390
390
  "-e",
391
+ ...hidden ? ["--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,8 +16312,15 @@ 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.",
16304
- parameters: { projectPath: { ...PROJECT_PATH, ops: ["open"] } },
16315
+ description: "Opens the Godot editor on a project, or restarts the one already connected. The editor is an ordinary visible one on the desktop in use, and on Windows it is shown without being activated, so opening or reopening it does not take the keyboard from whatever the user is typing in. hidden: true opens it headless instead and, on Windows, on a desktop of its own, where the games it plays go too; a hidden editor serves the bridge, the language server and the debug adapter as a visible one does. A game a visible editor plays opens as a window on the desktop in use. An editor goes on serving the addon it read at startup, so restart is what puts a gdharness upgrade into effect, and nothing else here restarts one; 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, visible unless hidden: true is passed again. 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.",
16316
+ parameters: {
16317
+ projectPath: { ...PROJECT_PATH, ops: ["open"] },
16318
+ hidden: {
16319
+ type: "boolean",
16320
+ ops: ["open", "restart"],
16321
+ description: "Open the editor headless and, on Windows, on a desktop of its own, so no window of it or of the games it plays appears. Default false: one visible editor, shown without taking the focus."
16322
+ }
16323
+ },
16305
16324
  requires: [],
16306
16325
  operations: {
16307
16326
  open: { summary: "open the editor on a project", requires: ["projectPath"] },
@@ -16326,6 +16345,11 @@ var init_tool_definitions = __esm(() => {
16326
16345
  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
16346
  },
16328
16347
  headless: { type: "boolean", ops: ["start"], description: "start: force a window or no window." },
16348
+ visible: {
16349
+ type: "boolean",
16350
+ ops: ["start"],
16351
+ 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 a visible editor plays opens on the desktop in use, and one a hidden editor (editor_launch hidden: true) plays goes on its desktop. Elsewhere than Windows the window shows either way. Default false."
16352
+ },
16329
16353
  savesIn: {
16330
16354
  type: "string",
16331
16355
  ops: ["start", "check"],
@@ -16656,7 +16680,7 @@ var init_tool_definitions = __esm(() => {
16656
16680
  },
16657
16681
  {
16658
16682
  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.",
16683
+ 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
16684
  parameters: {
16661
16685
  projectPath: RUNNING_PROJECT_PATH,
16662
16686
  pid: RUNNING_PID,
@@ -16923,7 +16947,6 @@ var init_headless_operations = __esm(() => {
16923
16947
  status: "get_import_status",
16924
16948
  options: "get_import_options",
16925
16949
  set_options: "set_import_options",
16926
- reimport: "reimport_resource",
16927
16950
  uid: "get_uid",
16928
16951
  refresh_classes: "refresh_class_cache"
16929
16952
  }),
@@ -16937,7 +16960,7 @@ var init_headless_operations = __esm(() => {
16937
16960
  })
16938
16961
  });
16939
16962
  ENGINE_PASSES = dictionary({
16940
- project_import: dictionary({ refresh_uids: "import" })
16963
+ project_import: dictionary({ refresh_uids: "import", reimport: "import" })
16941
16964
  });
16942
16965
  EDITOR_READS = dictionary({
16943
16966
  project_settings: dictionary({ get: "get_project_setting" })
@@ -33091,6 +33114,10 @@ var init_dap_client = __esm(() => {
33091
33114
  init_ports();
33092
33115
  });
33093
33116
 
33117
+ // src/desktop.ts
33118
+ var HIDDEN_DESKTOP = "gdharness";
33119
+ var init_desktop = () => {};
33120
+
33094
33121
  // src/editor-log.ts
33095
33122
  import {
33096
33123
  closeSync,
@@ -37128,6 +37155,7 @@ function withActualsPrinted(failed, printed) {
37128
37155
  return entry;
37129
37156
  }
37130
37157
  let detail = entry.detail;
37158
+ let recovered = false;
37131
37159
  for (const diff of diffs) {
37132
37160
  const wrong = ` but was
37133
37161
  '${diff.merged.replaceAll("\r", "")}'
@@ -37136,12 +37164,13 @@ function withActualsPrinted(failed, printed) {
37136
37164
  detail = detail.replace(wrong, () => ` but was
37137
37165
  '${diff.actual}'
37138
37166
  at ${diff.at}`);
37167
+ recovered = true;
37139
37168
  }
37140
37169
  }
37141
- const alike = /Expecting:\n '([\s\S]*)'\n but was\n '\1'\n\tat /.test(detail);
37142
- if (alike) {
37170
+ if (!recovered && /Expecting:\n '[\s\S]*'\n but was\n '[\s\S]*'\n\tat /.test(detail)) {
37171
+ const alike = /Expecting:\n '([\s\S]*)'\n but was\n '\1'\n\tat /.test(detail);
37143
37172
  detail = `${detail}
37144
- ${BOTH_SIDES_ALIKE_NOTE}`;
37173
+ ${alike ? BOTH_SIDES_ALIKE_NOTE : VALUE_NOT_RECOVERED_NOTE}`;
37145
37174
  }
37146
37175
  return detail === entry.detail ? entry : { ...entry, detail };
37147
37176
  });
@@ -37176,7 +37205,7 @@ function orphansPrinted(printed) {
37176
37205
  suites
37177
37206
  };
37178
37207
  }
37179
- var MalformedReportError, ENTITIES, ATTRIBUTE, NOTHING_RAN, NO_SUCH_PATH, ESCAPE, PRINTED_ACTUAL, COLOUR2, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary";
37208
+ var MalformedReportError, ENTITIES, ATTRIBUTE, NOTHING_RAN, NO_SUCH_PATH, ESCAPE, LOCATION = `'[^'\\n]*' in \\S+:\\d+`, PRINTED_ACTUAL, COLOUR2, SPELLED, BOTH_SIDES_ALIKE_NOTE = "[gdharness: the expected and the actual printed alike because gdUnit4 drops the marks that tell them apart when it writes its report, and the runner printed no copy of this one, so neither side above is the value the assertion found.]", VALUE_NOT_RECOVERED_NOTE = '[gdharness: the value after "but was" may not be the one the assertion found. gdUnit4 writes a failing string into its report as a character diff against the expected one with the marks dropped, which merges the two, and the runner printed no copy of this one to read the value back from.]', RUNNING_SUITE, ORPHANS, OVERALL = "Overall Summary";
37180
37209
  var init_junit = __esm(() => {
37181
37210
  MalformedReportError = class MalformedReportError extends Error {
37182
37211
  constructor(message) {
@@ -37189,7 +37218,7 @@ var init_junit = __esm(() => {
37189
37218
  NOTHING_RAN = /no test cases found/i;
37190
37219
  NO_SUCH_PATH = /given directory or file does not exists:\s*(\S+)/i;
37191
37220
  ESCAPE = String.fromCharCode(27);
37192
- PRINTED_ACTUAL = new RegExp(` but was\\n '([\\s\\S]*?)'((?:${ESCAPE}\\[[0-9;]*m)*)\\tat ('[^'\\n]*' in \\S+:\\d+)`, "g");
37221
+ PRINTED_ACTUAL = new RegExp(` but was\\n '((?:(?!\\tat ${LOCATION})[\\s\\S])*?)'((?:${ESCAPE}\\[[0-9;]*m)*)\\tat (${LOCATION})`, "g");
37193
37222
  COLOUR2 = new RegExp(`${ESCAPE}\\[([0-9;]*)m`, "g");
37194
37223
  SPELLED = {
37195
37224
  "<LF>": `
@@ -37786,7 +37815,9 @@ async function launchOutsideTheTree(spec, runtime = process.execPath, keeper = K
37786
37815
  command: spec.command,
37787
37816
  args: spec.args,
37788
37817
  envChanges: changesFrom(spec.env, process.env),
37789
- ...spec.run === undefined ? {} : { run: spec.run }
37818
+ ...spec.run === undefined ? {} : { run: spec.run },
37819
+ ...spec.desktop === undefined ? {} : { desktop: spec.desktop },
37820
+ ...spec.noActivate === true ? { noActivate: true } : {}
37790
37821
  });
37791
37822
  let printed = "";
37792
37823
  let complained = "";
@@ -37980,17 +38011,38 @@ var init_project_scan = __esm(() => {
37980
38011
  CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
37981
38012
  });
37982
38013
 
38014
+ // src/reimport.ts
38015
+ import { createHash as createHash5 } from "node:crypto";
38016
+ import { existsSync as existsSync15, readFileSync as readFileSync19, rmSync as rmSync10, statSync as statSync7, utimesSync, writeFileSync as writeFileSync14 } from "node:fs";
38017
+ import { basename as basename3, join as join21 } from "node:path";
38018
+ function forceNextImport(projectPath, resourcePath) {
38019
+ const relative = resourcePath.replace(/^res:\/\//, "");
38020
+ const sidecar = join21(projectPath, `${relative}.import`);
38021
+ if (!existsSync15(sidecar)) {
38022
+ return;
38023
+ }
38024
+ const { atime, mtimeMs } = statSync7(sidecar);
38025
+ writeFileSync14(sidecar, readFileSync19(sidecar, "utf8").replace(/^valid=false\r?\n/m, ""));
38026
+ utimesSync(sidecar, atime, new Date(mtimeMs + 2000));
38027
+ rmSync10(importRecord(projectPath, resourcePath), { force: true });
38028
+ }
38029
+ function importRecord(projectPath, resourcePath) {
38030
+ const digest = createHash5("md5").update(resourcePath).digest("hex");
38031
+ return join21(projectPath, ".godot", "imported", `${basename3(resourcePath)}-${digest}.md5`);
38032
+ }
38033
+ var init_reimport = () => {};
38034
+
37983
38035
  // 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";
38036
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync20, rmSync as rmSync11, writeFileSync as writeFileSync15 } from "node:fs";
38037
+ import { dirname as dirname15, join as join22 } from "node:path";
37986
38038
  function restartNotePath(projectPath) {
37987
- return join21(projectPath, ".godot", "gdharness-restart.json");
38039
+ return join22(projectPath, ".godot", "gdharness-restart.json");
37988
38040
  }
37989
38041
  function noteRestartBegun(note) {
37990
38042
  const path = restartNotePath(note.projectPath);
37991
38043
  try {
37992
38044
  mkdirSync12(dirname15(path), { recursive: true });
37993
- writeFileSync14(path, `${JSON.stringify(note, null, 2)}
38045
+ writeFileSync15(path, `${JSON.stringify(note, null, 2)}
37994
38046
  `, "utf8");
37995
38047
  } catch (error) {
37996
38048
  console.error(`[SERVER] Could not record the restart at ${path}: ${errorMessage(error)}`);
@@ -37998,7 +38050,7 @@ function noteRestartBegun(note) {
37998
38050
  }
37999
38051
  function restartOwed(projectPath) {
38000
38052
  try {
38001
- const parsed = JSON.parse(readFileSync19(restartNotePath(projectPath), "utf8"));
38053
+ const parsed = JSON.parse(readFileSync20(restartNotePath(projectPath), "utf8"));
38002
38054
  if (typeof parsed !== "object" || parsed === null) {
38003
38055
  return null;
38004
38056
  }
@@ -38013,7 +38065,7 @@ function restartOwed(projectPath) {
38013
38065
  }
38014
38066
  function restartSettled(projectPath) {
38015
38067
  try {
38016
- rmSync10(restartNotePath(projectPath), { force: true });
38068
+ rmSync11(restartNotePath(projectPath), { force: true });
38017
38069
  } catch (error) {
38018
38070
  console.error(`[SERVER] Could not clear the restart note: ${errorMessage(error)}`);
38019
38071
  }
@@ -38024,33 +38076,33 @@ var init_restart_note = __esm(() => {
38024
38076
 
38025
38077
  // src/run-record.ts
38026
38078
  import { execFileSync as execFileSync2 } from "node:child_process";
38027
- import { createHash as createHash5 } from "node:crypto";
38079
+ import { createHash as createHash6 } from "node:crypto";
38028
38080
  import {
38029
38081
  mkdirSync as mkdirSync13,
38030
38082
  openSync as openSync2,
38031
38083
  readdirSync as readdirSync8,
38032
- readFileSync as readFileSync20,
38084
+ readFileSync as readFileSync21,
38033
38085
  realpathSync as realpathSync3,
38034
- rmSync as rmSync11,
38035
- statSync as statSync7,
38036
- writeFileSync as writeFileSync15
38086
+ rmSync as rmSync12,
38087
+ statSync as statSync8,
38088
+ writeFileSync as writeFileSync16
38037
38089
  } from "node:fs";
38038
- import { basename as basename3, dirname as dirname16, join as join22, resolve as resolve5 } from "node:path";
38090
+ import { basename as basename4, dirname as dirname16, join as join23, resolve as resolve5 } from "node:path";
38039
38091
  function runsDirectory() {
38040
- return join22(runtimeDirectory(), "runs");
38092
+ return join23(runtimeDirectory(), "runs");
38041
38093
  }
38042
38094
  function realAsFarAsItExists(path) {
38043
38095
  const missing = [];
38044
38096
  let at = path;
38045
38097
  for (;; ) {
38046
38098
  try {
38047
- return join22(realpathSync3.native(at), ...missing);
38099
+ return join23(realpathSync3.native(at), ...missing);
38048
38100
  } catch {
38049
38101
  const parent = dirname16(at);
38050
38102
  if (parent === at) {
38051
38103
  return path;
38052
38104
  }
38053
- missing.unshift(basename3(at));
38105
+ missing.unshift(basename4(at));
38054
38106
  at = parent;
38055
38107
  }
38056
38108
  }
@@ -38058,26 +38110,26 @@ function realAsFarAsItExists(path) {
38058
38110
  function projectKey(projectPath) {
38059
38111
  const real = realAsFarAsItExists(resolve5(projectPath)).replace(/[\\/]+$/, "");
38060
38112
  const folded = process.platform === "win32" || process.platform === "darwin" ? real.toLowerCase() : real;
38061
- return createHash5("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
38113
+ return createHash6("sha256").update(folded.replaceAll("\\", "/")).digest("hex").slice(0, 16);
38062
38114
  }
38063
38115
  function runRecordPath(projectPath, directory = runtimeDirectory()) {
38064
- return join22(directory, "runs", `note-${projectKey(projectPath)}.json`);
38116
+ return join23(directory, "runs", `note-${projectKey(projectPath)}.json`);
38065
38117
  }
38066
38118
  function openTranscript(startedAt) {
38067
38119
  mkdirSync13(runsDirectory(), { recursive: true });
38068
- const path = join22(runsDirectory(), `run-${startedAt}.log`);
38120
+ const path = join23(runsDirectory(), `run-${startedAt}.log`);
38069
38121
  return { path, fd: openSync2(path, "a") };
38070
38122
  }
38071
38123
  function editorNotePath(projectPath, directory = runtimeDirectory()) {
38072
- return join22(directory, "runs", `editor-${projectKey(projectPath)}.json`);
38124
+ return join23(directory, "runs", `editor-${projectKey(projectPath)}.json`);
38073
38125
  }
38074
38126
  function writeEditorRunNote(note) {
38075
38127
  mkdirSync13(runsDirectory(), { recursive: true });
38076
- writeFileSync15(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
38128
+ writeFileSync16(editorNotePath(note.projectPath), JSON.stringify(note, null, 2), "utf8");
38077
38129
  }
38078
38130
  function readEditorRunNote(projectPath) {
38079
38131
  for (const directory of runtimeDirectories()) {
38080
- const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join22(directory, "runs", "editor-run.json"));
38132
+ const note = editorNoteAt(editorNotePath(projectPath, directory)) ?? editorNoteAt(join23(directory, "runs", "editor-run.json"));
38081
38133
  if (note !== null && isSameDirectory(note.projectPath, projectPath)) {
38082
38134
  return note;
38083
38135
  }
@@ -38087,7 +38139,7 @@ function readEditorRunNote(projectPath) {
38087
38139
  function editorNoteAt(path) {
38088
38140
  let parsed;
38089
38141
  try {
38090
- parsed = JSON.parse(readFileSync20(path, "utf8"));
38142
+ parsed = JSON.parse(readFileSync21(path, "utf8"));
38091
38143
  } catch {
38092
38144
  return null;
38093
38145
  }
@@ -38109,7 +38161,7 @@ function readRunRecord(projectPath) {
38109
38161
  if (own !== null) {
38110
38162
  return own;
38111
38163
  }
38112
- const shared = recordAt(join22(directory, "runs", SHARED_NOTE));
38164
+ const shared = recordAt(join23(directory, "runs", SHARED_NOTE));
38113
38165
  if (shared !== null && shared.projectPath !== "" && isSameDirectory(shared.projectPath, projectPath)) {
38114
38166
  return shared;
38115
38167
  }
@@ -38121,13 +38173,13 @@ function everyRunRecord() {
38121
38173
  for (const directory of runtimeDirectories()) {
38122
38174
  let entries;
38123
38175
  try {
38124
- entries = readdirSync8(join22(directory, "runs"));
38176
+ entries = readdirSync8(join23(directory, "runs"));
38125
38177
  } catch {
38126
38178
  continue;
38127
38179
  }
38128
38180
  for (const entry of entries) {
38129
38181
  if (entry === SHARED_NOTE || entry.startsWith("note-") && entry.endsWith(".json")) {
38130
- const record = recordAt(join22(directory, "runs", entry));
38182
+ const record = recordAt(join23(directory, "runs", entry));
38131
38183
  if (record !== null) {
38132
38184
  found.push(record);
38133
38185
  }
@@ -38139,7 +38191,7 @@ function everyRunRecord() {
38139
38191
  function recordAt(path) {
38140
38192
  let parsed;
38141
38193
  try {
38142
- parsed = JSON.parse(readFileSync20(path, "utf8"));
38194
+ parsed = JSON.parse(readFileSync21(path, "utf8"));
38143
38195
  } catch {
38144
38196
  return null;
38145
38197
  }
@@ -38185,7 +38237,7 @@ function runningAs(pid) {
38185
38237
  const began = startedAt(pid, null);
38186
38238
  const when = began === null ? {} : { startedAt: began };
38187
38239
  if (process.platform === "linux") {
38188
- const raw = readFileSync20(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
38240
+ const raw = readFileSync21(`/proc/${pid}/cmdline`, "utf8").replaceAll("\x00", " ").trim();
38189
38241
  return raw === "" ? null : { kind: "commandLine", text: raw, ...when };
38190
38242
  }
38191
38243
  const args = execFileSync2("ps", ["-p", String(pid), "-o", "args="], {
@@ -38287,7 +38339,7 @@ function judgeRun(record, running, needed) {
38287
38339
  if (running.startedAt !== undefined && running.startedAt - record.startedAt > SAME_RUN_WINDOW_MS) {
38288
38340
  return false;
38289
38341
  }
38290
- const engine = record.command === undefined ? null : basename3(record.command);
38342
+ const engine = record.command === undefined ? null : basename4(record.command);
38291
38343
  const said = process.platform === "win32" ? running.text.toLowerCase() : running.text;
38292
38344
  const wanted = engine === null || process.platform !== "win32" ? engine : engine.toLowerCase();
38293
38345
  if (wanted !== null && !said.includes(wanted)) {
@@ -38323,19 +38375,19 @@ function judgeAnnouncedGame(running, projectPath, announcedAt) {
38323
38375
  }
38324
38376
  function clearRunRecord(projectPath) {
38325
38377
  for (const directory of runtimeDirectories()) {
38326
- const shared = join22(directory, "runs", SHARED_NOTE);
38378
+ const shared = join23(directory, "runs", SHARED_NOTE);
38327
38379
  const sharedRecord = recordAt(shared);
38328
38380
  const ours = sharedRecord !== null && sharedRecord.projectPath !== "" && isSameDirectory(sharedRecord.projectPath, projectPath);
38329
38381
  for (const path of [runRecordPath(projectPath, directory), ...ours ? [shared] : []]) {
38330
38382
  try {
38331
- rmSync11(path, { force: true });
38383
+ rmSync12(path, { force: true });
38332
38384
  } catch {}
38333
38385
  }
38334
38386
  }
38335
38387
  }
38336
38388
  function ageMs(path, now) {
38337
38389
  try {
38338
- return now - statSync7(path).mtimeMs;
38390
+ return now - statSync8(path).mtimeMs;
38339
38391
  } catch {
38340
38392
  return 0;
38341
38393
  }
@@ -38353,12 +38405,12 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
38353
38405
  if (!entry.startsWith("run-") || !entry.endsWith(".log")) {
38354
38406
  continue;
38355
38407
  }
38356
- const path = join22(directory, entry);
38408
+ const path = join23(directory, entry);
38357
38409
  if (kept.has(path) || ageMs(path, now) < keepMs) {
38358
38410
  continue;
38359
38411
  }
38360
38412
  try {
38361
- rmSync11(path);
38413
+ rmSync12(path);
38362
38414
  } catch {}
38363
38415
  }
38364
38416
  }
@@ -38377,19 +38429,19 @@ import {
38377
38429
  closeSync as closeSync2,
38378
38430
  constants,
38379
38431
  copyFileSync,
38380
- existsSync as existsSync15,
38432
+ existsSync as existsSync16,
38381
38433
  fstatSync,
38382
38434
  mkdirSync as mkdirSync14,
38383
38435
  mkdtempSync as mkdtempSync3,
38384
38436
  openSync as openSync3,
38385
38437
  readdirSync as readdirSync9,
38386
- readFileSync as readFileSync21,
38438
+ readFileSync as readFileSync22,
38387
38439
  readSync as readSync2,
38388
38440
  rmdirSync,
38389
- statSync as statSync8
38441
+ statSync as statSync9
38390
38442
  } from "node:fs";
38391
38443
  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";
38444
+ 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
38445
  import { setTimeout as delay3 } from "node:timers/promises";
38394
38446
  import { fileURLToPath as fileURLToPath6 } from "node:url";
38395
38447
  import { promisify as promisify7 } from "node:util";
@@ -38404,15 +38456,15 @@ function captureDestinationRefusal(outputPath, projects) {
38404
38456
  return `outputPath ${outputPath} does not end in .png, which is what a capture is.`;
38405
38457
  }
38406
38458
  const directory = realPathOr(dirname17(outputPath));
38407
- if (!existsSync15(directory)) {
38459
+ if (!existsSync16(directory)) {
38408
38460
  return `outputPath's directory ${dirname17(outputPath)} does not exist; a capture creates no directories.`;
38409
38461
  }
38410
- const where = join23(directory, basename4(outputPath));
38462
+ const where = join24(directory, basename5(outputPath));
38411
38463
  const inside = projects.find((project) => project !== "" && isWithinRoot(realPathOr(project), where));
38412
38464
  if (inside !== undefined) {
38413
38465
  return `outputPath ${outputPath} is inside the project at ${inside}; a capture is saved outside it, so it never becomes part of the game.`;
38414
38466
  }
38415
- if (existsSync15(where)) {
38467
+ if (existsSync16(where)) {
38416
38468
  return `outputPath ${outputPath} already exists; a capture is not written over a file. Name a new one.`;
38417
38469
  }
38418
38470
  return null;
@@ -38769,6 +38821,38 @@ function scanWaitAnswer(scanned) {
38769
38821
  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
38822
  };
38771
38823
  }
38824
+ function changedSettings(before, after) {
38825
+ if (before === null || after === null) {
38826
+ return null;
38827
+ }
38828
+ const names = new Set([...before.keys(), ...after.keys()]);
38829
+ return [...names].filter((name) => JSON.stringify(before.get(name)) !== JSON.stringify(after.get(name))).sort();
38830
+ }
38831
+ function timedOutVerdict(timeoutMs, silentForMs) {
38832
+ const quiet = Math.round(silentForMs);
38833
+ return quiet >= Math.min(STALLED_AFTER_MS, timeoutMs / 2) ? {
38834
+ verdict: `hung: killed after ${timeoutMs} ms, having printed nothing for the last ${quiet} ms`,
38835
+ hung: true
38836
+ } : {
38837
+ verdict: `timed out after ${timeoutMs} ms while still running: it last printed ${quiet} ms before it was killed, so pass a longer timeoutMs or a narrower path`,
38838
+ hung: false
38839
+ };
38840
+ }
38841
+ function importStatuses(payload) {
38842
+ const listed = payload["resources"];
38843
+ if (!Array.isArray(listed)) {
38844
+ return [];
38845
+ }
38846
+ return listed.map((entry) => {
38847
+ const one = asParams(entry);
38848
+ const reason = readString(one, "reason");
38849
+ return {
38850
+ path: readString(one, "path") ?? "",
38851
+ status: readString(one, "status") ?? "unknown",
38852
+ ...reason === undefined ? {} : { reason }
38853
+ };
38854
+ });
38855
+ }
38772
38856
  function escapingPropertyValue(properties) {
38773
38857
  if (typeof properties !== "object" || properties === null) {
38774
38858
  return;
@@ -38799,7 +38883,7 @@ function aboveTheRunner(entry) {
38799
38883
  }
38800
38884
  function elsewhereIn(projectPath, asked) {
38801
38885
  const wanted = asked.replace(/^res:\/\//, "").replace(/\/+$/, "");
38802
- if (wanted === "" || existsSync15(join23(projectPath, wanted))) {
38886
+ if (wanted === "" || existsSync16(join24(projectPath, wanted))) {
38803
38887
  return "";
38804
38888
  }
38805
38889
  const root = (wanted.split("/")[0] ?? "").toLowerCase();
@@ -38813,7 +38897,7 @@ function elsewhereIn(projectPath, asked) {
38813
38897
  return near.length === 0 ? "" : ` The project does have ${there}.`;
38814
38898
  }
38815
38899
  function hasMainScene(projectFile) {
38816
- const scene = parseProjectGodot(readFileSync21(projectFile, "utf8"))["application"]?.["run/main_scene"];
38900
+ const scene = parseProjectGodot(readFileSync22(projectFile, "utf8"))["application"]?.["run/main_scene"];
38817
38901
  return typeof scene === "string" && scene !== "";
38818
38902
  }
38819
38903
  function runtimeAddonAnnouncesEditors(projectPath) {
@@ -38821,13 +38905,13 @@ function runtimeAddonAnnouncesEditors(projectPath) {
38821
38905
  return false;
38822
38906
  }
38823
38907
  try {
38824
- return readFileSync21(join23(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
38908
+ return readFileSync22(join24(projectPath, RUNTIME_AUTOLOAD2.path), "utf8").includes('"editor_pid"');
38825
38909
  } catch {
38826
38910
  return false;
38827
38911
  }
38828
38912
  }
38829
38913
  function editorPlaysHeadless(projectFile) {
38830
- const runArgs = parseProjectGodot(readFileSync21(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
38914
+ const runArgs = parseProjectGodot(readFileSync22(projectFile, "utf8"))["editor"]?.["run/main_run_args"];
38831
38915
  return typeof runArgs === "string" && /(?:^|\s)--headless(?:\s|$)/.test(runArgs);
38832
38916
  }
38833
38917
  function camelCased(params) {
@@ -38846,7 +38930,7 @@ function needsItsOwnProcess(asked) {
38846
38930
  class GodotServer {
38847
38931
  mcp;
38848
38932
  locator = new GodotLocator2;
38849
- operationsScript = join23(__dirname2, "godot", "operations", "godot_operations.gd");
38933
+ operationsScript = join24(__dirname2, "godot", "operations", "godot_operations.gd");
38850
38934
  godotBridge;
38851
38935
  tools = buildToolDefinitions();
38852
38936
  activeProcess = null;
@@ -39132,9 +39216,12 @@ class GodotServer {
39132
39216
  try {
39133
39217
  return await this.dispatch(tool, op, args);
39134
39218
  } catch (error) {
39135
- if (error instanceof McpError || error instanceof Refusal) {
39219
+ if (error instanceof McpError) {
39136
39220
  throw error;
39137
39221
  }
39222
+ if (error instanceof Refusal) {
39223
+ return this.createErrorResponse(error.message);
39224
+ }
39138
39225
  const where = op === "" ? tool : `${tool} op=${op}`;
39139
39226
  console.error(`[SERVER] Unmodelled failure in ${where}:`, error);
39140
39227
  const seenBefore = this.defectsSeen.before(where, errorMessage(error));
@@ -39291,7 +39378,14 @@ class GodotServer {
39291
39378
  }
39292
39379
  async dispatch(tool, op, args) {
39293
39380
  if (ENGINE_PASSES[tool]?.[op] !== undefined) {
39294
- return await this.handleRefreshUids(args);
39381
+ return op === "reimport" ? await this.handleReimport(args) : await this.handleRefreshUids(args);
39382
+ }
39383
+ const settingsWrite = tool === "project_settings" && op !== "get" ? HEADLESS_OPERATIONS[tool]?.[op] : undefined;
39384
+ if (settingsWrite !== undefined) {
39385
+ return await this.handleProjectSettingsWrite(op, settingsWrite, args);
39386
+ }
39387
+ if (tool === "project_import" && op === "set_options") {
39388
+ return await this.handleSetImportOptions(args);
39295
39389
  }
39296
39390
  const headless = HEADLESS_OPERATIONS[tool]?.[op];
39297
39391
  if (headless !== undefined) {
@@ -39407,7 +39501,7 @@ class GodotServer {
39407
39501
  return await bridge("set_theme_font_size", { themePath: args["resourcePath"] });
39408
39502
  }
39409
39503
  case "editor_launch":
39410
- return op === "restart" ? await this.handleRestartEditor() : await this.handleLaunchEditor(args);
39504
+ return op === "restart" ? await this.handleRestartEditor(args) : await this.handleLaunchEditor(args);
39411
39505
  case "editor_run":
39412
39506
  if (op === "stop") {
39413
39507
  return await this.handleStopProject(args);
@@ -39550,8 +39644,8 @@ class GodotServer {
39550
39644
  if (path === undefined) {
39551
39645
  return { ok: false, response: this.createErrorResponse("projectPath is required.") };
39552
39646
  }
39553
- const file = join23(path, "project.godot");
39554
- if (!existsSync15(file)) {
39647
+ const file = join24(path, "project.godot");
39648
+ if (!existsSync16(file)) {
39555
39649
  return {
39556
39650
  ok: false,
39557
39651
  response: this.createErrorResponse(`Not a Godot project: ${path}`, [
@@ -39822,6 +39916,222 @@ class GodotServer {
39822
39916
  }
39823
39917
  }, scanned));
39824
39918
  }
39919
+ async handleReimport(args) {
39920
+ const project = this.project(args);
39921
+ if (!project.ok) {
39922
+ return project.response;
39923
+ }
39924
+ const contained = this.containProjectFiles(args);
39925
+ if (!contained.ok) {
39926
+ return contained.response;
39927
+ }
39928
+ const outcome = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), args["force"] === true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
39929
+ return outcome.ok ? this.jsonTextResponse(outcome.payload) : outcome.response;
39930
+ }
39931
+ editorHasOpen(projectPath) {
39932
+ const open = this.godotBridge.getStatus().projectPath;
39933
+ return this.godotBridge.isConnected() && open !== undefined && open !== "" && isSameDirectory(open, projectPath);
39934
+ }
39935
+ async handleProjectSettingsWrite(op, operation, args) {
39936
+ const project = this.project(args);
39937
+ if (!project.ok) {
39938
+ return project.response;
39939
+ }
39940
+ const projectPath = project.value.path;
39941
+ const served = this.editorHasOpen(projectPath);
39942
+ const before = served ? this.settingKeysOf(projectPath) : null;
39943
+ const written = await this.headless(operation, args);
39944
+ if (!served || written.isError === true) {
39945
+ return written;
39946
+ }
39947
+ const answer = asParams(JSON.parse(written.content[0]?.text ?? "{}"));
39948
+ if (AUDIO_BUS_OPS.has(op)) {
39949
+ return this.jsonTextResponse({ ...answer, ...await this.editorTakes("adopt_audio_bus_layout", {}) });
39950
+ }
39951
+ const changed = changedSettings(before, this.settingKeysOf(projectPath));
39952
+ if (changed === null) {
39953
+ return this.jsonTextResponse({
39954
+ ...answer,
39955
+ 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."
39956
+ });
39957
+ }
39958
+ if (changed.length === 0) {
39959
+ return written;
39960
+ }
39961
+ const taken = await this.editorTakes("adopt_project_settings", { settings: changed });
39962
+ return this.jsonTextResponse({
39963
+ ...answer,
39964
+ ...taken,
39965
+ ...taken["editorNote"] === undefined ? { editorAdopted: changed } : {}
39966
+ });
39967
+ }
39968
+ async editorTakes(command, args) {
39969
+ try {
39970
+ await this.godotBridge.invokeTool(command, args);
39971
+ return {};
39972
+ } catch (error) {
39973
+ const message = errorMessage(error);
39974
+ const why = message.includes("Unknown tool") ? "runs an addon from before writes were handed to it" : `could not take the change (${message})`;
39975
+ return {
39976
+ 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.`
39977
+ };
39978
+ }
39979
+ }
39980
+ async handleSetImportOptions(args) {
39981
+ const project = this.project(args);
39982
+ if (!project.ok) {
39983
+ return project.response;
39984
+ }
39985
+ const contained = this.containProjectFiles(args);
39986
+ if (!contained.ok) {
39987
+ return contained.response;
39988
+ }
39989
+ const {
39990
+ op: _op,
39991
+ projectPath: _projectPath,
39992
+ reimport: _reimport,
39993
+ timeoutMs: _timeoutMs,
39994
+ ...params
39995
+ } = contained.value;
39996
+ const written = await this.operation("set_import_options", params, project.value.path);
39997
+ if (!written.ok || args["reimport"] === false) {
39998
+ return this.answer(written.ok ? {
39999
+ ...written,
40000
+ payload: {
40001
+ ...written.payload,
40002
+ note: "Not reimported: the editor imports it with these options on its next scan, or project_import reimport does now."
40003
+ }
40004
+ } : written);
40005
+ }
40006
+ const applied = await this.reimport(project.value.path, readNonEmptyString(contained.value, "resourcePath"), true, readPositiveNumber(args, "timeoutMs") ?? REIMPORT_TIMEOUT_MS);
40007
+ if (!applied.ok) {
40008
+ return applied.response;
40009
+ }
40010
+ return this.jsonTextResponse({ ...written.payload, reimport: applied.payload });
40011
+ }
40012
+ async reimport(projectPath, resourcePath, force, timeoutMs) {
40013
+ const ask = resourcePath === undefined ? { includeUpToDate: force } : { resourcePath };
40014
+ const before = await this.operation("get_import_status", ask, projectPath);
40015
+ if (!before.ok) {
40016
+ return { ok: false, response: this.answer(before) };
40017
+ }
40018
+ const listed = importStatuses(before.payload);
40019
+ const missingSource = listed.filter((one) => one.status === "missing_source").map((one) => one.path);
40020
+ if (resourcePath !== undefined && missingSource.length > 0) {
40021
+ return {
40022
+ ok: false,
40023
+ response: this.createErrorResponse(`${missingSource[0]} is not on disk, so there is nothing to import.`)
40024
+ };
40025
+ }
40026
+ const targets = listed.filter((one) => one.status !== "missing_source" && (force || one.status !== "up_to_date")).map((one) => one.path);
40027
+ if (targets.length === 0) {
40028
+ return {
40029
+ ok: true,
40030
+ payload: {
40031
+ reimported: [],
40032
+ notReimported: [],
40033
+ 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.`
40034
+ }
40035
+ };
40036
+ }
40037
+ const viaEditor = this.editorHasOpen(projectPath);
40038
+ const extra = {};
40039
+ if (viaEditor) {
40040
+ const inEditor = await this.reimportInTheEditor(targets, timeoutMs);
40041
+ if (!inEditor.ok) {
40042
+ return inEditor;
40043
+ }
40044
+ if (!inEditor.finished) {
40045
+ return {
40046
+ ok: true,
40047
+ payload: {
40048
+ via: "editor",
40049
+ stillImporting: true,
40050
+ asked: targets,
40051
+ 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.`
40052
+ }
40053
+ };
40054
+ }
40055
+ } else {
40056
+ const engine = await this.engine();
40057
+ if (!engine.ok) {
40058
+ return { ok: false, response: engine.response };
40059
+ }
40060
+ for (const target of targets) {
40061
+ forceNextImport(projectPath, target);
40062
+ }
40063
+ const scanned = await this.untilTheEditorsScanIsWritten(projectPath);
40064
+ const imported = await runImport(engine.value, projectPath);
40065
+ if (!imported.ok) {
40066
+ return { ok: false, response: this.answer(withScanWait(imported, scanned)) };
40067
+ }
40068
+ if (imported.messages.length > 0) {
40069
+ extra["engine_messages"] = imported.messages;
40070
+ }
40071
+ }
40072
+ const after = await this.operation("get_import_status", resourcePath === undefined ? { includeUpToDate: true } : { resourcePath }, projectPath);
40073
+ if (!after.ok) {
40074
+ return { ok: false, response: this.answer(after) };
40075
+ }
40076
+ const now = new Map(importStatuses(after.payload).map((one) => [one.path, one]));
40077
+ const reimported = targets.filter((path) => now.get(path)?.status === "up_to_date");
40078
+ const notReimported = targets.filter((path) => now.get(path)?.status !== "up_to_date").map((path) => ({
40079
+ path,
40080
+ status: now.get(path)?.status ?? "unknown",
40081
+ ...now.get(path)?.reason === undefined ? {} : { reason: now.get(path)?.reason }
40082
+ }));
40083
+ return {
40084
+ ok: true,
40085
+ payload: {
40086
+ via: viaEditor ? "editor" : "engine",
40087
+ reimported,
40088
+ notReimported,
40089
+ ...missingSource.length > 0 ? { missingSource } : {},
40090
+ ...extra
40091
+ }
40092
+ };
40093
+ }
40094
+ async reimportInTheEditor(paths, timeoutMs) {
40095
+ const started = Date.now();
40096
+ let completedBefore;
40097
+ for (;; ) {
40098
+ while (Date.now() - started < timeoutMs && await this.editorIsScanningOrImporting()) {
40099
+ await new Promise((settle) => setTimeout(settle, 100));
40100
+ }
40101
+ let answer;
40102
+ try {
40103
+ answer = asParams(await this.godotBridge.invokeTool("reimport_files", { paths: [...paths] }));
40104
+ } catch (error) {
40105
+ const message = errorMessage(error);
40106
+ return {
40107
+ ok: false,
40108
+ response: message.includes("Unknown tool") ? this.createErrorResponse("The editor is running an addon from before reimport_files, so it cannot be asked to reimport.", [
40109
+ "editor_launch restart loads the addon this server ships",
40110
+ "Or close the editor, and project_import reimport runs the engine import instead"
40111
+ ]) : this.createErrorResponse(`The editor answered reimport_files with an error: ${message}`)
40112
+ };
40113
+ }
40114
+ if (answer["started"] !== false) {
40115
+ completedBefore = readNumber(answer, "reimportsCompletedBefore");
40116
+ break;
40117
+ }
40118
+ if (Date.now() - started >= timeoutMs) {
40119
+ return { ok: true, finished: false };
40120
+ }
40121
+ await new Promise((settle) => setTimeout(settle, 100));
40122
+ }
40123
+ while (Date.now() - started < timeoutMs) {
40124
+ await new Promise((settle) => setTimeout(settle, 250));
40125
+ try {
40126
+ const status = asParams(await this.godotBridge.invokeTool("scan_status", {}));
40127
+ const completed = readNumber(status, "reimportsCompleted");
40128
+ if (completedBefore !== undefined && completed !== undefined && completed > completedBefore) {
40129
+ return { ok: true, finished: true };
40130
+ }
40131
+ } catch {}
40132
+ }
40133
+ return { ok: true, finished: false };
40134
+ }
39825
40135
  async operation(operation, params, projectPath) {
39826
40136
  const engine = await this.engine();
39827
40137
  if (!engine.ok) {
@@ -39863,11 +40173,11 @@ class GodotServer {
39863
40173
  if (!engine.ok) {
39864
40174
  return engine.response;
39865
40175
  }
39866
- const application = parseProjectGodot(readFileSync21(project.value.file, "utf8"))["application"] ?? {};
40176
+ const application = parseProjectGodot(readFileSync22(project.value.file, "utf8"))["application"] ?? {};
39867
40177
  const name = application["config/name"];
39868
40178
  const mainScene = application["run/main_scene"];
39869
40179
  const info = {
39870
- name: typeof name === "string" && name !== "" ? name : basename4(project.value.path),
40180
+ name: typeof name === "string" && name !== "" ? name : basename5(project.value.path),
39871
40181
  path: project.value.path,
39872
40182
  mainScene: typeof mainScene === "string" && mainScene !== "" ? mainScene : null,
39873
40183
  godotVersion: await this.godotVersion(engine.value),
@@ -39931,11 +40241,11 @@ class GodotServer {
39931
40241
  return this.createErrorResponse(`Cannot create ${dirname17(output.relativePath)} to export into: ${errorMessage(error)}`);
39932
40242
  }
39933
40243
  const debug = readBoolean(args, "debug") ?? false;
39934
- const exportLogs = mkdtempSync3(join23(tmpdir5(), "gdharness-export-"));
40244
+ const exportLogs = mkdtempSync3(join24(tmpdir5(), "gdharness-export-"));
39935
40245
  const exportArgs = [
39936
40246
  "--headless",
39937
40247
  "--log-file",
39938
- join23(exportLogs, "engine.log"),
40248
+ join24(exportLogs, "engine.log"),
39939
40249
  "--path",
39940
40250
  project.value.path,
39941
40251
  debug ? "--export-debug" : "--export-release",
@@ -39967,7 +40277,7 @@ class GodotServer {
39967
40277
  log.finish();
39968
40278
  const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
39969
40279
  const verdict = {
39970
- exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync15(output.absolutePath),
40280
+ exported: ending.exitCode === 0 && log.count("error") === 0 && existsSync16(output.absolutePath),
39971
40281
  preset,
39972
40282
  outputPath: output.relativePath,
39973
40283
  debug,
@@ -40003,7 +40313,7 @@ class GodotServer {
40003
40313
  return contained.response;
40004
40314
  }
40005
40315
  const runner = "addons/gdUnit4/bin/GdUnitCmdTool.gd";
40006
- if (!existsSync15(join23(project.value.path, runner))) {
40316
+ if (!existsSync16(join24(project.value.path, runner))) {
40007
40317
  return this.createErrorResponse(`gdUnit4 is not installed in this project: no ${runner}.`, [
40008
40318
  "Install gdUnit4 under addons/gdUnit4, from https://github.com/godot-gdunit-labs/gdUnit4"
40009
40319
  ]);
@@ -40039,14 +40349,21 @@ class GodotServer {
40039
40349
  ];
40040
40350
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
40041
40351
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
40042
- const userData = mkdtempSync3(join23(tmpdir5(), "gdharness-tests-"));
40352
+ const userData = mkdtempSync3(join24(tmpdir5(), "gdharness-tests-"));
40043
40353
  const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
40044
40354
  const console2 = [];
40355
+ let lastSaid = Date.now();
40045
40356
  run.process.stdout?.on("data", (data) => {
40046
40357
  console2.push(data);
40358
+ lastSaid = Date.now();
40047
40359
  });
40048
- const hung = await new Promise((resolve) => {
40360
+ run.process.stderr?.on("data", () => {
40361
+ lastSaid = Date.now();
40362
+ });
40363
+ let silentForMs = 0;
40364
+ const timedOut = await new Promise((resolve) => {
40049
40365
  const timer = setTimeout(() => {
40366
+ silentForMs = Date.now() - lastSaid;
40050
40367
  run.process.kill();
40051
40368
  resolve(true);
40052
40369
  }, timeoutMs);
@@ -40059,14 +40376,14 @@ class GodotServer {
40059
40376
  resolve(false);
40060
40377
  });
40061
40378
  });
40062
- const reportsDir = join23(project.value.path, ".godot", ...ours.split("/"));
40379
+ const reportsDir = join24(project.value.path, ".godot", ...ours.split("/"));
40063
40380
  let report = null;
40064
40381
  let reportProblem = null;
40065
40382
  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))) : [];
40383
+ 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
40384
  const newest = written.at(-1);
40068
40385
  if (newest !== undefined) {
40069
- report = parseJUnit(readFileSync21(join23(reportsDir, newest, "results.xml"), "utf8"));
40386
+ report = parseJUnit(readFileSync22(join24(reportsDir, newest, "results.xml"), "utf8"));
40070
40387
  }
40071
40388
  } catch (error) {
40072
40389
  reportProblem = errorMessage(error);
@@ -40088,8 +40405,10 @@ class GodotServer {
40088
40405
  const exitCode = run.exitCode;
40089
40406
  const printed = run.log.select({ severity: "info", sinceLastCall: false, limit: 200 }).entries;
40090
40407
  const said = printed.map((entry) => entry.text);
40091
- const nothingRan = hung ? null : whyNoReport(said, asked);
40092
- const verdict = hung ? `hung: killed after ${timeoutMs} ms` : nothingRan ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
40408
+ const nothingRan = timedOut ? null : whyNoReport(said, asked);
40409
+ const cutShort = timedOut ? timedOutVerdict(timeoutMs, silentForMs) : null;
40410
+ const hung = cutShort?.hung ?? false;
40411
+ const verdict = cutShort !== null ? cutShort.verdict : nothingRan ?? (exitCode === null ? undefined : verdicts[exitCode]) ?? (run.exitSignal === null ? `exit ${exitCode ?? "unknown"}` : howItExited(run));
40093
40412
  const savesNote = savesStayPut() && nothingRan === null ? SAVES_NOT_MOVED_NOTE : undefined;
40094
40413
  if (report === null) {
40095
40414
  const note = nothingRan === null ? `The test run wrote no report (${verdict}${reportProblem ? `; ${reportProblem}` : ""}).` : `No tests ran: ${verdict}.${elsewhereIn(project.value.path, asked)}`;
@@ -40105,6 +40424,7 @@ class GodotServer {
40105
40424
  exitCode,
40106
40425
  exitSignal: run.exitSignal ?? undefined,
40107
40426
  hung,
40427
+ ...timedOut ? { timedOut, silentForMs } : {},
40108
40428
  arguments: cmdArgs,
40109
40429
  entries: forAnswer(printed.slice(0, 60)),
40110
40430
  savesNote,
@@ -40129,8 +40449,9 @@ class GodotServer {
40129
40449
  const unclean = report.suites.filter((suite) => suite.failures > 0 || suite.errors > 0 || suite.skipped > 0 || orphansIn(suite.path) > 0);
40130
40450
  const notRun = report.suites.reduce((sum, suite) => sum + Math.max(0, suite.discovered - suite.tests), 0);
40131
40451
  return this.jsonTextResponse({
40132
- passed: !hung && exitCode === 0 && report.failures === 0 && report.errors === 0,
40452
+ passed: !timedOut && exitCode === 0 && report.failures === 0 && report.errors === 0,
40133
40453
  verdict,
40454
+ ...timedOut ? { timedOut, hung, silentForMs } : {},
40134
40455
  exitCode,
40135
40456
  exitSignal: run.exitSignal ?? undefined,
40136
40457
  tests: report.tests,
@@ -40209,11 +40530,11 @@ class GodotServer {
40209
40530
  const cached = cachedClasses(projectPath);
40210
40531
  const contradicted = cached === null ? [] : contradictedDiagnostics(messages, cached, (resourcePath) => {
40211
40532
  const contained = resolveWithinProject(projectPath, resourcePath);
40212
- if (!contained.ok || !existsSync15(contained.absolutePath)) {
40533
+ if (!contained.ok || !existsSync16(contained.absolutePath)) {
40213
40534
  return null;
40214
40535
  }
40215
40536
  try {
40216
- return readFileSync21(contained.absolutePath, "utf8");
40537
+ return readFileSync22(contained.absolutePath, "utf8");
40217
40538
  } catch {
40218
40539
  return null;
40219
40540
  }
@@ -40263,11 +40584,11 @@ class GodotServer {
40263
40584
  }
40264
40585
  sourceOfClass(projectPath, resourcePath) {
40265
40586
  const contained = resolveWithinProject(projectPath, resourcePath);
40266
- if (!contained.ok || !existsSync15(contained.absolutePath)) {
40587
+ if (!contained.ok || !existsSync16(contained.absolutePath)) {
40267
40588
  return null;
40268
40589
  }
40269
40590
  try {
40270
- return readFileSync21(contained.absolutePath, "utf8");
40591
+ return readFileSync22(contained.absolutePath, "utf8");
40271
40592
  } catch {
40272
40593
  return null;
40273
40594
  }
@@ -40536,12 +40857,12 @@ class GodotServer {
40536
40857
  return null;
40537
40858
  }
40538
40859
  try {
40539
- return settingKeys(readFileSync21(join23(projectPath, "project.godot"), "utf8"));
40860
+ return settingKeys(readFileSync22(join24(projectPath, "project.godot"), "utf8"));
40540
40861
  } catch {
40541
40862
  return null;
40542
40863
  }
40543
40864
  }
40544
- async handleRestartEditor() {
40865
+ async handleRestartEditor(args) {
40545
40866
  const before = this.godotBridge.getStatus();
40546
40867
  if (!before.connected) {
40547
40868
  return this.createErrorResponse("No editor is connected, so there is nothing to restart.", [
@@ -40551,7 +40872,7 @@ class GodotServer {
40551
40872
  }
40552
40873
  const settingsBefore = this.settingKeysOf(before.projectPath);
40553
40874
  const mine = before.openedByAServer === true && before.projectPath !== undefined;
40554
- const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid) : await this.handleViaBridge("restart_editor", {});
40875
+ const asked = mine ? await this.startItAgain(before.projectPath ?? "", before.editorPid, args["hidden"] === true) : await this.handleViaBridge("restart_editor", {});
40555
40876
  if (asked.isError === true) {
40556
40877
  return asked;
40557
40878
  }
@@ -40589,7 +40910,7 @@ class GodotServer {
40589
40910
  whatTheEditorDropped(before, after) {
40590
40911
  return settingsDroppedReport(before, after);
40591
40912
  }
40592
- async startItAgain(projectPath, editorPid) {
40913
+ async startItAgain(projectPath, editorPid, hidden) {
40593
40914
  const engine = await this.engine();
40594
40915
  if (!engine.ok) {
40595
40916
  return engine.response;
@@ -40611,7 +40932,7 @@ class GodotServer {
40611
40932
  return asked;
40612
40933
  }
40613
40934
  await this.waitForBridge(() => !alive(editorPid), Date.now() + EDITOR_RESTART_TIMEOUT_MS);
40614
- const opened = await this.openAnEditor(engine.value, projectPath, ports);
40935
+ const opened = await this.openAnEditor(engine.value, projectPath, ports, hidden);
40615
40936
  restartSettled(projectPath);
40616
40937
  if (opened.error !== null) {
40617
40938
  return this.createErrorResponse(`The editor was asked to go and could not be started again: ${opened.error}`, [`editor_launch opens one on ${projectPath}`]);
@@ -40649,7 +40970,7 @@ class GodotServer {
40649
40970
  const ports = await this.portsForAnEditor();
40650
40971
  const before = this.settingKeysOf(project.value.path);
40651
40972
  const unfinished = restartOwed(project.value.path);
40652
- const opened = await this.openAnEditor(engine.value, project.value.path, ports);
40973
+ const opened = await this.openAnEditor(engine.value, project.value.path, ports, args["hidden"] === true);
40653
40974
  if (opened.error !== null) {
40654
40975
  return this.createErrorResponse(`Could not start the editor: ${opened.error}`);
40655
40976
  }
@@ -40665,18 +40986,19 @@ class GodotServer {
40665
40986
  settingsNote: before === null ? "project.godot could not be read before this editor was opened, so nothing here will be able to say what the import saved away. Opening a project rewrites the file and drops any key sitting at its own default." : before.size === 0 ? undefined : "Opening a project imports it and saves project.godot, and Godot drops any key sitting at its own default. editor_status names anything lost under settingsDropped once this editor has connected."
40666
40987
  });
40667
40988
  }
40668
- async openAnEditor(engine, projectPath, ports) {
40989
+ async openAnEditor(engine, projectPath, ports, hidden) {
40669
40990
  clearEditorLog(projectPath);
40670
40991
  const launched = await launchOutsideTheTree({
40671
40992
  command: engine,
40672
- args: editorArguments(projectPath, ports, editorLogPath(projectPath)),
40993
+ args: editorArguments(projectPath, ports, editorLogPath(projectPath), hidden),
40673
40994
  env: {
40674
40995
  ...process.env,
40675
40996
  GDHARNESS_RUNTIME_DIR: runtimeDirectory(),
40676
40997
  GDHARNESS_LSP_PORT: String(ports.lsp),
40677
40998
  GDHARNESS_DAP_PORT: String(ports.dap),
40678
40999
  [OPENED_BY_A_SERVER]: "1"
40679
- }
41000
+ },
41001
+ ...hidden ? { desktop: HIDDEN_DESKTOP } : { noActivate: true }
40680
41002
  });
40681
41003
  if ("error" in launched) {
40682
41004
  return { pid: null, error: launched.error };
@@ -40758,7 +41080,7 @@ class GodotServer {
40758
41080
  userArgs: given.value
40759
41081
  });
40760
41082
  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);
41083
+ 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
41084
  if ("error" in started) {
40763
41085
  return this.createErrorResponse(`The game could not be started: ${started.error}`, [
40764
41086
  "editor_status says which Godot this server runs and whether it answers"
@@ -40788,7 +41110,7 @@ class GodotServer {
40788
41110
  }
40789
41111
  async runtimeUp(projectPath, before, budget, withArgs) {
40790
41112
  const budgetMs = budget.ms;
40791
- if (!existsSync15(join23(projectPath, RUNTIME_AUTOLOAD2.path))) {
41113
+ if (!existsSync16(join24(projectPath, RUNTIME_AUTOLOAD2.path))) {
40792
41114
  return runtimeVerdict(null, { addon: false, budgetMs, heldAt: null, running: false, withArgs });
40793
41115
  }
40794
41116
  let asking = null;
@@ -40851,7 +41173,7 @@ class GodotServer {
40851
41173
  }
40852
41174
  let announcedAt;
40853
41175
  try {
40854
- announcedAt = statSync8(endpoint.file).mtimeMs;
41176
+ announcedAt = statSync9(endpoint.file).mtimeMs;
40855
41177
  } catch {
40856
41178
  return;
40857
41179
  }
@@ -41098,7 +41420,7 @@ class GodotServer {
41098
41420
  });
41099
41421
  return started;
41100
41422
  }
41101
- async launchKeptGame(godotPath, cmdArgs, projectPath, env) {
41423
+ async launchKeptGame(godotPath, cmdArgs, projectPath, env, desktop) {
41102
41424
  const startedAt = Date.now();
41103
41425
  const transcript = openTranscript(startedAt);
41104
41426
  closeSync2(transcript.fd);
@@ -41106,7 +41428,8 @@ class GodotServer {
41106
41428
  command: godotPath,
41107
41429
  args: cmdArgs,
41108
41430
  ...env === undefined ? {} : { env },
41109
- run: { transcript: transcript.path, startedAt, projectPath }
41431
+ run: { transcript: transcript.path, startedAt, projectPath },
41432
+ ...desktop === undefined ? {} : { desktop }
41110
41433
  });
41111
41434
  if ("error" in launched) {
41112
41435
  return launched;
@@ -41601,7 +41924,7 @@ class GodotServer {
41601
41924
  notes.push(endedWithoutACode({
41602
41925
  throughEditor: run.throughEditor,
41603
41926
  announced: announced !== undefined || run.announcedPid !== undefined,
41604
- couldAnnounce: run.projectPath !== null && existsSync15(join23(run.projectPath, RUNTIME_AUTOLOAD2.path)),
41927
+ couldAnnounce: run.projectPath !== null && existsSync16(join24(run.projectPath, RUNTIME_AUTOLOAD2.path)),
41605
41928
  endedHere: typeof run.endedHere === "string",
41606
41929
  pickedUp: run.pickedUp === true
41607
41930
  }));
@@ -42114,7 +42437,7 @@ class GodotServer {
42114
42437
  async ownRunOnItsWay(projectPath, budgetMs) {
42115
42438
  const run = this.currentRun();
42116
42439
  const project = projectPath ?? run?.projectPath ?? null;
42117
- 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)) {
42440
+ 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)) {
42118
42441
  return null;
42119
42442
  }
42120
42443
  const already = runtimesAnnounced().running;
@@ -42123,7 +42446,7 @@ class GodotServer {
42123
42446
  return !run.announcedBefore.has(one.pid);
42124
42447
  }
42125
42448
  try {
42126
- return statSync8(one.file).mtimeMs >= run.startedAt;
42449
+ return statSync9(one.file).mtimeMs >= run.startedAt;
42127
42450
  } catch {
42128
42451
  return false;
42129
42452
  }
@@ -42193,8 +42516,8 @@ class GodotServer {
42193
42516
  return this.createErrorResponse(refusal);
42194
42517
  }
42195
42518
  }
42196
- const screenshotDir = expectsScreenshot ? mkdtempSync3(join23(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
42197
- const screenshotPath = screenshotDir ? join23(screenshotDir, "capture.png") : null;
42519
+ const screenshotDir = expectsScreenshot ? mkdtempSync3(join24(tmpdir5(), "gdharness-runtime-screenshot-")) : null;
42520
+ const screenshotPath = screenshotDir ? join24(screenshotDir, "capture.png") : null;
42198
42521
  try {
42199
42522
  const reply = await runtimeRequest(choice.endpoint, command, screenshotPath ? { ...params, output_path: screenshotPath } : params, timeoutMs, !expectsScreenshot);
42200
42523
  if (!reply.ok && reply.requestId !== undefined) {
@@ -42235,7 +42558,7 @@ class GodotServer {
42235
42558
  type: "text",
42236
42559
  text: `Screenshot captured: ${dimensions}${chosenAmongSeveral ? ` from pid ${own}, the game this server holds` : ""}${saveTo === null ? "" : `. Saved to ${saveTo}`}`
42237
42560
  },
42238
- { type: "image", data: readFileSync21(screenshotPath).toString("base64"), mimeType: "image/png" }
42561
+ { type: "image", data: readFileSync22(screenshotPath).toString("base64"), mimeType: "image/png" }
42239
42562
  ]
42240
42563
  };
42241
42564
  } finally {
@@ -42335,7 +42658,7 @@ async function runGodotServer() {
42335
42658
  const server = new GodotServer;
42336
42659
  await server.run();
42337
42660
  }
42338
- 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/";
42661
+ 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, STALLED_AFTER_MS = 30000, WAIT_FOR_RUN_MS = 600000, RUN_POLL_MS = 250, PATH_SOLUTIONS, PROJECT_FILE_ARGUMENTS, DEBUG_STATE_CALLS, PROJECT_INFO_SECTIONS2, RUNNER_DIRECTORY = "addons/gdUnit4/";
42339
42662
  var init_server3 = __esm(() => {
42340
42663
  init_mcp();
42341
42664
  init_stdio2();
@@ -42348,6 +42671,7 @@ var init_server3 = __esm(() => {
42348
42671
  init_class_note();
42349
42672
  init_config_pin();
42350
42673
  init_dap_client();
42674
+ init_desktop();
42351
42675
  init_editor_log();
42352
42676
  init_errors();
42353
42677
  init_game_log();
@@ -42364,6 +42688,7 @@ var init_server3 = __esm(() => {
42364
42688
  init_process_children();
42365
42689
  init_process_time();
42366
42690
  init_project_scan();
42691
+ init_reimport();
42367
42692
  init_resources();
42368
42693
  init_restart_note();
42369
42694
  init_run_record();
@@ -42384,6 +42709,11 @@ var init_server3 = __esm(() => {
42384
42709
  object: "an object"
42385
42710
  };
42386
42711
  HANDOVER_MS = SUCCESSOR_CHECK_MS + 5000;
42712
+ AUDIO_BUS_OPS = new Set([
42713
+ "add_audio_bus",
42714
+ "set_audio_bus_effect",
42715
+ "set_audio_bus_volume"
42716
+ ]);
42387
42717
  PATH_SOLUTIONS = [
42388
42718
  'Give the path relative to the project, such as "scenes/main.tscn" or "res://scenes/main.tscn"',
42389
42719
  "Point projectPath at the project the file belongs to"
@@ -42415,8 +42745,8 @@ var init_server3 = __esm(() => {
42415
42745
  });
42416
42746
 
42417
42747
  // src/cli.ts
42418
- import { existsSync as existsSync16 } from "fs";
42419
- import { join as join24, resolve as resolve6 } from "path";
42748
+ import { existsSync as existsSync17 } from "fs";
42749
+ import { join as join25, resolve as resolve6 } from "path";
42420
42750
 
42421
42751
  // src/bridge-announce.ts
42422
42752
  init_errors();
@@ -44050,7 +44380,9 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
44050
44380
  | A picture, for a person who asked to see one | \`runtime_capture\` |
44051
44381
 
44052
44382
  These need the runtime autoload, which \`gdharness setup\` registers. \`runtime_capture\` needs a
44053
- window and refuses headless rather than handing back the last frame anything drew.
44383
+ window and refuses headless rather than handing back the last frame anything drew. On Windows a
44384
+ windowed run this server starts is on a desktop of its own, where it draws and can be captured but
44385
+ never shows or takes the keyboard.
44054
44386
 
44055
44387
  The autoload reads these settings and no others. \`${BIND_ADDRESS_SETTING}\`
44056
44388
  is \`127.0.0.1\` and should stay there: the command set includes \`call_method\`, \`set_property\`
@@ -44102,7 +44434,7 @@ them. \`editor_rescan\` asks it to scan and names any class it still cannot reso
44102
44434
  \`unseenByEditor\`: its walk skips a file a headless engine has already imported, so the
44103
44435
  declaration and the cache can both be right while the editor stays blind to it. Rescan again on
44104
44436
  its own, which is the measured cure and needs no change to the declaring script; \`editor_launch
44105
- restart\` also does it and costs a window.
44437
+ restart\` also does it and costs a restart of the editor.
44106
44438
 
44107
44439
  The scan writes \`.godot/global_script_class_cache.cfg\` from the list the editor is holding, so
44108
44440
  a class it cannot resolve would go out of that file with it. The answer names that loss under
@@ -44240,7 +44572,7 @@ class UsageError extends Refusal2 {
44240
44572
  function projectArgument(at) {
44241
44573
  const named = args.slice(at).find((value) => !value.startsWith("--"));
44242
44574
  const projectPath = resolve6(named ?? ".");
44243
- if (!existsSync16(join24(projectPath, "project.godot"))) {
44575
+ if (!existsSync17(join25(projectPath, "project.godot"))) {
44244
44576
  throw new UsageError(`Not a Godot project: ${projectPath} holds no project.godot.`);
44245
44577
  }
44246
44578
  return projectPath;
@@ -44279,7 +44611,7 @@ async function registerRuntime(godot, projectPath) {
44279
44611
  return;
44280
44612
  }
44281
44613
  console.log(`${RUNTIME_AUTOLOAD.name} autoload left at ${named}, which this project registers itself`);
44282
- if (!existsSync16(join24(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
44614
+ if (!existsSync17(join25(projectPath, (named ?? "").replace(/^res:\/\//, "")))) {
44283
44615
  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}`);
44284
44616
  }
44285
44617
  }
@@ -44411,7 +44743,7 @@ async function setup() {
44411
44743
  console.log("no harness configured; name one yourself, and gdharness harnesses lists them all");
44412
44744
  }
44413
44745
  if (!args.includes("--no-skill")) {
44414
- const directories = skillDirectories(chosen.harnesses, projectPath, existsSync16);
44746
+ const directories = skillDirectories(chosen.harnesses, projectPath, existsSync17);
44415
44747
  for (const written of writeSkill(directories, getLocalVersion())) {
44416
44748
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
44417
44749
  }
@@ -44505,7 +44837,7 @@ async function upgrade() {
44505
44837
  moved.push(took.from);
44506
44838
  }
44507
44839
  }
44508
- for (const written of writeSkill(skillDirectories(already, projectPath, existsSync16), version)) {
44840
+ for (const written of writeSkill(skillDirectories(already, projectPath, existsSync17), version)) {
44509
44841
  console.log(`skill: ${written.replaced ? "replaced" : "written"} ${written.path}`);
44510
44842
  }
44511
44843
  console.log(`