gdharness 0.5.0 → 0.5.2

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
@@ -160,6 +160,19 @@ function runArguments(options) {
160
160
  function editorArguments(projectPath) {
161
161
  return ["-e", "--path", projectPath];
162
162
  }
163
+ function userDataIn(home, variables = process.env) {
164
+ const moved = ["APPDATA", "XDG_DATA_HOME"];
165
+ const carried = {};
166
+ for (const [name, value] of Object.entries(variables)) {
167
+ if (!moved.some((named) => named.toLowerCase() === name.toLowerCase())) {
168
+ carried[name] = value;
169
+ }
170
+ }
171
+ for (const named of moved) {
172
+ carried[named] = home;
173
+ }
174
+ return carried;
175
+ }
163
176
 
164
177
  // node_modules/yaml/dist/nodes/identity.js
165
178
  var require_identity = __commonJS(function(exports) {
@@ -7228,15 +7241,196 @@ var init_game_log = __esm(() => {
7228
7241
  COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
7229
7242
  });
7230
7243
 
7244
+ // src/update-check.ts
7245
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
7246
+ import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
7247
+ import { join as join4 } from "node:path";
7248
+ function cacheDirectory(environment) {
7249
+ const set = (name) => {
7250
+ const value = environment[name];
7251
+ return value !== undefined && value !== "" ? value : null;
7252
+ };
7253
+ const home = set("HOME") ?? homedir3();
7254
+ if (process.platform === "win32") {
7255
+ return join4(set("LOCALAPPDATA") ?? home, "gdharness");
7256
+ }
7257
+ if (process.platform === "darwin") {
7258
+ return join4(home, "Library", "Caches", "gdharness");
7259
+ }
7260
+ return join4(set("XDG_CACHE_HOME") ?? join4(home, ".cache"), "gdharness");
7261
+ }
7262
+ function cacheFile(environment = process.env) {
7263
+ try {
7264
+ const directory = cacheDirectory(environment);
7265
+ mkdirSync2(directory, { recursive: true, mode: 448 });
7266
+ return join4(directory, "update-check.json");
7267
+ } catch {
7268
+ return join4(tmpdir2(), "gdharness-update-check.json");
7269
+ }
7270
+ }
7271
+ function readCache(path) {
7272
+ try {
7273
+ if (!existsSync4(path)) {
7274
+ return null;
7275
+ }
7276
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
7277
+ if (typeof parsed !== "object" || parsed === null) {
7278
+ return null;
7279
+ }
7280
+ const record = parsed;
7281
+ const checkedAt = record["checkedAt"];
7282
+ const latest = record["latest"];
7283
+ if (typeof checkedAt !== "number" || typeof latest !== "string" || !VERSION.test(latest)) {
7284
+ return null;
7285
+ }
7286
+ return { checkedAt, latest };
7287
+ } catch {
7288
+ return null;
7289
+ }
7290
+ }
7291
+ function writeCache(path, entry) {
7292
+ try {
7293
+ writeFileSync3(path, JSON.stringify(entry), { encoding: "utf8", mode: 384 });
7294
+ } catch {}
7295
+ }
7296
+ function parts(version) {
7297
+ const withoutBuild = version.split("+")[0] ?? version;
7298
+ const dash = withoutBuild.indexOf("-");
7299
+ const numeric = dash === -1 ? withoutBuild : withoutBuild.slice(0, dash);
7300
+ return {
7301
+ numbers: numeric.split(".").map((piece) => Number.parseInt(piece, 10) || 0),
7302
+ prerelease: dash !== -1
7303
+ };
7304
+ }
7305
+ function isNewer(candidate, current) {
7306
+ const left = parts(candidate);
7307
+ const right = parts(current);
7308
+ for (let index = 0;index < 3; index += 1) {
7309
+ const a = left.numbers[index] ?? 0;
7310
+ const b = right.numbers[index] ?? 0;
7311
+ if (a !== b) {
7312
+ return a > b;
7313
+ }
7314
+ }
7315
+ return !left.prerelease && right.prerelease;
7316
+ }
7317
+ async function fetchLatest() {
7318
+ const response = await fetch(REGISTRY, {
7319
+ headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
7320
+ redirect: "error",
7321
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
7322
+ });
7323
+ if (!response.ok || response.body === null) {
7324
+ return null;
7325
+ }
7326
+ const declared = Number.parseInt(response.headers.get("content-length") ?? "", 10);
7327
+ if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
7328
+ return null;
7329
+ }
7330
+ const chunks = [];
7331
+ let size = 0;
7332
+ for await (const chunk of response.body) {
7333
+ size += chunk.byteLength;
7334
+ if (size > MAX_BODY_BYTES) {
7335
+ return null;
7336
+ }
7337
+ chunks.push(chunk);
7338
+ }
7339
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
7340
+ if (typeof parsed !== "object" || parsed === null) {
7341
+ return null;
7342
+ }
7343
+ const version = parsed["version"];
7344
+ return typeof version === "string" && VERSION.test(version) ? version : null;
7345
+ }
7346
+
7347
+ class UpdateCheck {
7348
+ latest = null;
7349
+ checking = false;
7350
+ checkedAt = 0;
7351
+ retryAt = 0;
7352
+ backoffMs = FIRST_RETRY_MS;
7353
+ enabled;
7354
+ current;
7355
+ cachePath;
7356
+ constructor(current, environment = process.env) {
7357
+ this.current = current;
7358
+ this.enabled = (environment["GDHARNESS_NO_UPDATE_CHECK"] ?? "") === "";
7359
+ this.cachePath = cacheFile(environment);
7360
+ const cached = this.enabled ? readCache(this.cachePath) : null;
7361
+ if (cached !== null) {
7362
+ this.latest = cached.latest;
7363
+ this.checkedAt = cached.checkedAt;
7364
+ }
7365
+ }
7366
+ refresh(now = Date.now()) {
7367
+ if (!this.enabled || this.checking || now < this.retryAt || now - this.checkedAt < CACHE_MS) {
7368
+ return;
7369
+ }
7370
+ this.checking = true;
7371
+ fetchLatest().then((version) => {
7372
+ if (version === null) {
7373
+ this.scheduleRetry(now);
7374
+ return;
7375
+ }
7376
+ this.latest = version;
7377
+ this.checkedAt = Date.now();
7378
+ this.backoffMs = FIRST_RETRY_MS;
7379
+ this.retryAt = 0;
7380
+ writeCache(this.cachePath, { checkedAt: this.checkedAt, latest: version });
7381
+ }).catch(() => {
7382
+ this.scheduleRetry(now);
7383
+ }).finally(() => {
7384
+ this.checking = false;
7385
+ });
7386
+ }
7387
+ scheduleRetry(now) {
7388
+ this.retryAt = now + this.backoffMs;
7389
+ this.backoffMs = Math.min(this.backoffMs * 2, MAX_RETRY_MS);
7390
+ }
7391
+ notice() {
7392
+ const latest = this.latest;
7393
+ if (!this.enabled || latest === null || !isNewer(latest, this.current)) {
7394
+ return null;
7395
+ }
7396
+ return {
7397
+ current: this.current,
7398
+ latest,
7399
+ releaseNotes: `${RELEASES}/v${latest}`,
7400
+ upgrade: runLine(currentRunner(), latest, "upgrade")
7401
+ };
7402
+ }
7403
+ }
7404
+ var REGISTRY = "https://registry.npmjs.org/gdharness/latest", RELEASES = "https://github.com/Aureliolo/gdharness/releases/tag", CACHE_MS, REQUEST_TIMEOUT_MS = 1e4, MAX_BODY_BYTES, FIRST_RETRY_MS = 30000, MAX_RETRY_MS, VERSION;
7405
+ var init_update_check = __esm(() => {
7406
+ CACHE_MS = 4 * 60 * 60 * 1000;
7407
+ MAX_BODY_BYTES = 1 << 20;
7408
+ MAX_RETRY_MS = 60 * 60 * 1000;
7409
+ VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
7410
+ });
7411
+
7231
7412
  // src/server-version.ts
7232
- import { readFileSync as readFileSync2 } from "node:fs";
7233
- var DEBUG_MODE, GODOT_DEBUG_MODE_DEFAULT, SERVER_VERSION;
7413
+ import { readFileSync as readFileSync3 } from "node:fs";
7414
+ function addonMismatch(addonVersion, serverVersion) {
7415
+ if (addonVersion === serverVersion) {
7416
+ return;
7417
+ }
7418
+ const reported = addonVersion ?? "";
7419
+ const editor = reported === "" ? UNVERSIONED : reported;
7420
+ const both = `The editor is running the ${editor} addon while this server ships ${serverVersion}.`;
7421
+ if (reported !== "" && isNewer(reported, serverVersion)) {
7422
+ return `${both} This server is the older half: reconnect it in your harness so it spawns ${reported}.`;
7423
+ }
7424
+ return `${both} Restart it with editor_launch restart to pick the new one up.`;
7425
+ }
7426
+ var DEBUG_MODE, GODOT_DEBUG_MODE_DEFAULT, SERVER_VERSION, UNVERSIONED = "addon from before versions were reported";
7234
7427
  var init_server_version = __esm(() => {
7428
+ init_update_check();
7235
7429
  DEBUG_MODE = process.env["DEBUG"] === "true";
7236
7430
  GODOT_DEBUG_MODE_DEFAULT = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE;
7237
7431
  SERVER_VERSION = (() => {
7238
7432
  try {
7239
- const pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
7433
+ const pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
7240
7434
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
7241
7435
  } catch {
7242
7436
  return "0.0.0";
@@ -7245,8 +7439,8 @@ var init_server_version = __esm(() => {
7245
7439
  });
7246
7440
 
7247
7441
  // src/class-cache.ts
7248
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
7249
- import { join as join4 } from "node:path";
7442
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
7443
+ import { join as join5 } from "node:path";
7250
7444
  function declaredClasses(projectPath) {
7251
7445
  const declared = new Map;
7252
7446
  const visit = (directory, prefix) => {
@@ -7254,11 +7448,11 @@ function declaredClasses(projectPath) {
7254
7448
  if (entry.name.startsWith(".")) {
7255
7449
  continue;
7256
7450
  }
7257
- const path = join4(directory, entry.name);
7451
+ const path = join5(directory, entry.name);
7258
7452
  if (entry.isDirectory()) {
7259
7453
  visit(path, `${prefix}${entry.name}/`);
7260
7454
  } else if (entry.isFile() && entry.name.endsWith(".gd")) {
7261
- const found = /^class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m.exec(readFileSync4(path, "utf8"));
7455
+ const found = /^class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m.exec(readFileSync5(path, "utf8"));
7262
7456
  if (found?.[1]) {
7263
7457
  declared.set(found[1], `res://${prefix}${entry.name}`);
7264
7458
  }
@@ -7269,12 +7463,12 @@ function declaredClasses(projectPath) {
7269
7463
  return declared;
7270
7464
  }
7271
7465
  function cachedClasses(projectPath) {
7272
- const cache = join4(projectPath, ".godot", "global_script_class_cache.cfg");
7273
- if (!existsSync4(cache)) {
7466
+ const cache = join5(projectPath, ".godot", "global_script_class_cache.cfg");
7467
+ if (!existsSync5(cache)) {
7274
7468
  return null;
7275
7469
  }
7276
7470
  const listed = new Map;
7277
- const text = readFileSync4(cache, "utf8");
7471
+ const text = readFileSync5(cache, "utf8");
7278
7472
  for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
7279
7473
  listed.set(entry[1] ?? "", entry[2] ?? "");
7280
7474
  }
@@ -7291,9 +7485,9 @@ var init_class_cache = () => {};
7291
7485
 
7292
7486
  // src/headless.ts
7293
7487
  import { execFile as execFile3 } from "node:child_process";
7294
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
7295
- import { tmpdir as tmpdir2 } from "node:os";
7296
- import { join as join5 } from "node:path";
7488
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
7489
+ import { tmpdir as tmpdir3 } from "node:os";
7490
+ import { join as join6 } from "node:path";
7297
7491
  import { promisify as promisify3 } from "node:util";
7298
7492
  function snakeCased2(params) {
7299
7493
  const result = emptyRecord();
@@ -7333,9 +7527,9 @@ function reason2(stdout, stderr) {
7333
7527
  return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
7334
7528
  }
7335
7529
  async function runOperation2(engine, operation, params, projectPath) {
7336
- const paramsDir = mkdtempSync2(join5(tmpdir2(), "gdharness-params-"));
7337
- const paramsFile = join5(paramsDir, `${operation}.json`);
7338
- writeFileSync3(paramsFile, JSON.stringify(snakeCased2(params)), "utf8");
7530
+ const paramsDir = mkdtempSync2(join6(tmpdir3(), "gdharness-params-"));
7531
+ const paramsFile = join6(paramsDir, `${operation}.json`);
7532
+ writeFileSync4(paramsFile, JSON.stringify(snakeCased2(params)), "utf8");
7339
7533
  const args = [
7340
7534
  "--headless",
7341
7535
  "--path",
@@ -14537,7 +14731,7 @@ var init_paths = __esm(() => {
14537
14731
  });
14538
14732
 
14539
14733
  // src/resources.ts
14540
- import { readFileSync as readFileSync5 } from "node:fs";
14734
+ import { readFileSync as readFileSync6 } from "node:fs";
14541
14735
  import { extname, resolve as resolve2 } from "node:path";
14542
14736
  function ensureProjectPath(getProjectPath) {
14543
14737
  const projectPath = getProjectPath();
@@ -14679,7 +14873,7 @@ function readResourceText(uri, getProjectPath) {
14679
14873
  const parsedUri = parseGodotUri(uri);
14680
14874
  if (parsedUri.kind === "project-info") {
14681
14875
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
14682
- const rawProject = readFileSync5(projectFilePath, "utf-8");
14876
+ const rawProject = readFileSync6(projectFilePath, "utf-8");
14683
14877
  const parsedProject = parseProjectGodot(rawProject);
14684
14878
  return {
14685
14879
  mimeType: "application/json",
@@ -14688,7 +14882,7 @@ function readResourceText(uri, getProjectPath) {
14688
14882
  }
14689
14883
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
14690
14884
  ensureAllowedExtension(parsedUri.kind, filePath);
14691
- const text = readFileSync5(filePath, "utf-8");
14885
+ const text = readFileSync6(filePath, "utf-8");
14692
14886
  return {
14693
14887
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
14694
14888
  text
@@ -15157,7 +15351,7 @@ var init_tool_definitions = __esm(() => {
15157
15351
  },
15158
15352
  {
15159
15353
  name: "project_test",
15160
- description: "Runs the project's gdUnit4 tests headless and answers with every case: which failed, where, and what the assertion said. The class list is rebuilt first, so a suite written a moment ago is found. Needs gdUnit4 under addons/gdUnit4.",
15354
+ description: "Runs the project's gdUnit4 tests headless and answers with every case: which failed, where, and what the assertion said. The class list is rebuilt first, so a suite written a moment ago is found. On Windows and Linux the run gets a user:// of its own, so a suite that saves a game writes nowhere near the saves of the copy somebody plays. Needs gdUnit4 under addons/gdUnit4.",
15161
15355
  parameters: {
15162
15356
  projectPath: PROJECT_PATH,
15163
15357
  path: {
@@ -34297,7 +34491,7 @@ var init_godot_bridge = __esm(() => {
34297
34491
 
34298
34492
  // src/godot-path.ts
34299
34493
  import { execFile as execFile4 } from "node:child_process";
34300
- import { existsSync as existsSync7 } from "node:fs";
34494
+ import { existsSync as existsSync8 } from "node:fs";
34301
34495
  import { normalize as normalize2 } from "node:path";
34302
34496
  import { promisify as promisify4 } from "node:util";
34303
34497
  var run4, GodotLocator2;
@@ -34339,7 +34533,7 @@ var init_godot_path = __esm(() => {
34339
34533
  return known;
34340
34534
  }
34341
34535
  let ok = false;
34342
- if (path === "godot" || existsSync7(path)) {
34536
+ if (path === "godot" || existsSync8(path)) {
34343
34537
  try {
34344
34538
  await run4(path, ["--version"]);
34345
34539
  ok = true;
@@ -35142,8 +35336,8 @@ var init_lsp_client = __esm(() => {
35142
35336
  });
35143
35337
 
35144
35338
  // src/project-scan.ts
35145
- import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "node:fs";
35146
- import { join as join9 } from "node:path";
35339
+ import { readdirSync as readdirSync4, readFileSync as readFileSync9 } from "node:fs";
35340
+ import { join as join10 } from "node:path";
35147
35341
  function projectStructure(projectPath) {
35148
35342
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
35149
35343
  const visit = (directory) => {
@@ -35152,7 +35346,7 @@ function projectStructure(projectPath) {
35152
35346
  continue;
35153
35347
  }
35154
35348
  if (entry.isDirectory()) {
35155
- visit(join9(directory, entry.name));
35349
+ visit(join10(directory, entry.name));
35156
35350
  } else if (entry.isFile()) {
35157
35351
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
35158
35352
  if (extension === "tscn") {
@@ -35194,7 +35388,7 @@ function searchProject(projectPath, options) {
35194
35388
  if (SKIPPED.has(entry.name)) {
35195
35389
  continue;
35196
35390
  }
35197
- const entryPath = join9(directory, entry.name);
35391
+ const entryPath = join10(directory, entry.name);
35198
35392
  if (entry.isDirectory()) {
35199
35393
  visit(entryPath);
35200
35394
  continue;
@@ -35205,7 +35399,7 @@ function searchProject(projectPath, options) {
35205
35399
  }
35206
35400
  result.summary.files_searched += 1;
35207
35401
  const matches = [];
35208
- for (const [index, line] of readFileSync8(entryPath, "utf8").split(`
35402
+ for (const [index, line] of readFileSync9(entryPath, "utf8").split(`
35209
35403
  `).entries()) {
35210
35404
  if (full()) {
35211
35405
  break;
@@ -35233,17 +35427,25 @@ var init_project_scan = __esm(() => {
35233
35427
  });
35234
35428
 
35235
35429
  // src/runtime-client.ts
35236
- import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync9, unlinkSync } from "node:fs";
35430
+ import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync10, unlinkSync } from "node:fs";
35237
35431
  import { createConnection as createConnection3 } from "node:net";
35238
- import { tmpdir as tmpdir3 } from "node:os";
35239
- import { join as join10, resolve as resolve4 } from "node:path";
35432
+ import { tmpdir as tmpdir4 } from "node:os";
35433
+ import { join as join11, resolve as resolve4 } from "node:path";
35240
35434
  function runtimeDirectory(variables = process.env) {
35241
35435
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
35242
35436
  if (explicit) {
35243
35437
  return explicit;
35244
35438
  }
35245
35439
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
35246
- return join10(perUser ?? tmpdir3(), "gdharness");
35440
+ return join11(perUser ?? tmpdir4(), "gdharness");
35441
+ }
35442
+ function runtimeDirectories(variables = process.env) {
35443
+ const candidates = [runtimeDirectory(variables)];
35444
+ const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
35445
+ for (const base of fallbacks) {
35446
+ candidates.push(join11(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
35447
+ }
35448
+ return [...new Set(candidates.map((path) => resolve4(path)))];
35247
35449
  }
35248
35450
  function processAlive(pid) {
35249
35451
  try {
@@ -35256,7 +35458,7 @@ function processAlive(pid) {
35256
35458
  function parseAnnouncement(file, pid) {
35257
35459
  let fields;
35258
35460
  try {
35259
- fields = asParams(JSON.parse(readFileSync9(file, "utf8")));
35461
+ fields = asParams(JSON.parse(readFileSync10(file, "utf8")));
35260
35462
  } catch {
35261
35463
  return null;
35262
35464
  }
@@ -35274,8 +35476,19 @@ function parseAnnouncement(file, pid) {
35274
35476
  file
35275
35477
  };
35276
35478
  }
35277
- function discoverRuntimes(directory = runtimeDirectory()) {
35278
- if (!existsSync8(directory)) {
35479
+ function discoverRuntimes(directories = runtimeDirectories()) {
35480
+ const found = new Map;
35481
+ for (const directory of directories) {
35482
+ for (const endpoint of announcedIn(directory)) {
35483
+ if (!found.has(endpoint.pid)) {
35484
+ found.set(endpoint.pid, endpoint);
35485
+ }
35486
+ }
35487
+ }
35488
+ return [...found.values()].sort((a, b) => b.pid - a.pid);
35489
+ }
35490
+ function announcedIn(directory) {
35491
+ if (!existsSync9(directory)) {
35279
35492
  return [];
35280
35493
  }
35281
35494
  const found = [];
@@ -35284,7 +35497,7 @@ function discoverRuntimes(directory = runtimeDirectory()) {
35284
35497
  if (!match) {
35285
35498
  continue;
35286
35499
  }
35287
- const file = join10(directory, entry);
35500
+ const file = join11(directory, entry);
35288
35501
  const pid = Number.parseInt(match[1] ?? "", 10);
35289
35502
  const endpoint = processAlive(pid) ? parseAnnouncement(file, pid) : null;
35290
35503
  if (endpoint) {
@@ -35295,7 +35508,7 @@ function discoverRuntimes(directory = runtimeDirectory()) {
35295
35508
  } catch {}
35296
35509
  }
35297
35510
  }
35298
- return found.sort((a, b) => b.pid - a.pid);
35511
+ return found;
35299
35512
  }
35300
35513
  function describe2(endpoint) {
35301
35514
  return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
@@ -35420,174 +35633,6 @@ var init_runtime_client = __esm(() => {
35420
35633
  ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
35421
35634
  });
35422
35635
 
35423
- // src/update-check.ts
35424
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync6 } from "node:fs";
35425
- import { homedir as homedir3, tmpdir as tmpdir4 } from "node:os";
35426
- import { join as join11 } from "node:path";
35427
- function cacheDirectory(environment) {
35428
- const set = (name) => {
35429
- const value = environment[name];
35430
- return value !== undefined && value !== "" ? value : null;
35431
- };
35432
- const home = set("HOME") ?? homedir3();
35433
- if (process.platform === "win32") {
35434
- return join11(set("LOCALAPPDATA") ?? home, "gdharness");
35435
- }
35436
- if (process.platform === "darwin") {
35437
- return join11(home, "Library", "Caches", "gdharness");
35438
- }
35439
- return join11(set("XDG_CACHE_HOME") ?? join11(home, ".cache"), "gdharness");
35440
- }
35441
- function cacheFile(environment = process.env) {
35442
- try {
35443
- const directory = cacheDirectory(environment);
35444
- mkdirSync4(directory, { recursive: true, mode: 448 });
35445
- return join11(directory, "update-check.json");
35446
- } catch {
35447
- return join11(tmpdir4(), "gdharness-update-check.json");
35448
- }
35449
- }
35450
- function readCache(path) {
35451
- try {
35452
- if (!existsSync9(path)) {
35453
- return null;
35454
- }
35455
- const parsed = JSON.parse(readFileSync10(path, "utf8"));
35456
- if (typeof parsed !== "object" || parsed === null) {
35457
- return null;
35458
- }
35459
- const record = parsed;
35460
- const checkedAt = record["checkedAt"];
35461
- const latest = record["latest"];
35462
- if (typeof checkedAt !== "number" || typeof latest !== "string" || !VERSION.test(latest)) {
35463
- return null;
35464
- }
35465
- return { checkedAt, latest };
35466
- } catch {
35467
- return null;
35468
- }
35469
- }
35470
- function writeCache(path, entry) {
35471
- try {
35472
- writeFileSync6(path, JSON.stringify(entry), { encoding: "utf8", mode: 384 });
35473
- } catch {}
35474
- }
35475
- function parts(version) {
35476
- const withoutBuild = version.split("+")[0] ?? version;
35477
- const dash = withoutBuild.indexOf("-");
35478
- const numeric = dash === -1 ? withoutBuild : withoutBuild.slice(0, dash);
35479
- return {
35480
- numbers: numeric.split(".").map((piece) => Number.parseInt(piece, 10) || 0),
35481
- prerelease: dash !== -1
35482
- };
35483
- }
35484
- function isNewer(candidate, current) {
35485
- const left = parts(candidate);
35486
- const right = parts(current);
35487
- for (let index = 0;index < 3; index += 1) {
35488
- const a = left.numbers[index] ?? 0;
35489
- const b = right.numbers[index] ?? 0;
35490
- if (a !== b) {
35491
- return a > b;
35492
- }
35493
- }
35494
- return !left.prerelease && right.prerelease;
35495
- }
35496
- async function fetchLatest() {
35497
- const response = await fetch(REGISTRY, {
35498
- headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
35499
- redirect: "error",
35500
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
35501
- });
35502
- if (!response.ok || response.body === null) {
35503
- return null;
35504
- }
35505
- const declared = Number.parseInt(response.headers.get("content-length") ?? "", 10);
35506
- if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
35507
- return null;
35508
- }
35509
- const chunks = [];
35510
- let size = 0;
35511
- for await (const chunk of response.body) {
35512
- size += chunk.byteLength;
35513
- if (size > MAX_BODY_BYTES) {
35514
- return null;
35515
- }
35516
- chunks.push(chunk);
35517
- }
35518
- const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
35519
- if (typeof parsed !== "object" || parsed === null) {
35520
- return null;
35521
- }
35522
- const version = parsed["version"];
35523
- return typeof version === "string" && VERSION.test(version) ? version : null;
35524
- }
35525
-
35526
- class UpdateCheck {
35527
- latest = null;
35528
- checking = false;
35529
- checkedAt = 0;
35530
- retryAt = 0;
35531
- backoffMs = FIRST_RETRY_MS;
35532
- enabled;
35533
- current;
35534
- cachePath;
35535
- constructor(current, environment = process.env) {
35536
- this.current = current;
35537
- this.enabled = (environment["GDHARNESS_NO_UPDATE_CHECK"] ?? "") === "";
35538
- this.cachePath = cacheFile(environment);
35539
- const cached = this.enabled ? readCache(this.cachePath) : null;
35540
- if (cached !== null) {
35541
- this.latest = cached.latest;
35542
- this.checkedAt = cached.checkedAt;
35543
- }
35544
- }
35545
- refresh(now = Date.now()) {
35546
- if (!this.enabled || this.checking || now < this.retryAt || now - this.checkedAt < CACHE_MS) {
35547
- return;
35548
- }
35549
- this.checking = true;
35550
- fetchLatest().then((version) => {
35551
- if (version === null) {
35552
- this.scheduleRetry(now);
35553
- return;
35554
- }
35555
- this.latest = version;
35556
- this.checkedAt = Date.now();
35557
- this.backoffMs = FIRST_RETRY_MS;
35558
- this.retryAt = 0;
35559
- writeCache(this.cachePath, { checkedAt: this.checkedAt, latest: version });
35560
- }).catch(() => {
35561
- this.scheduleRetry(now);
35562
- }).finally(() => {
35563
- this.checking = false;
35564
- });
35565
- }
35566
- scheduleRetry(now) {
35567
- this.retryAt = now + this.backoffMs;
35568
- this.backoffMs = Math.min(this.backoffMs * 2, MAX_RETRY_MS);
35569
- }
35570
- notice() {
35571
- const latest = this.latest;
35572
- if (!this.enabled || latest === null || !isNewer(latest, this.current)) {
35573
- return null;
35574
- }
35575
- return {
35576
- current: this.current,
35577
- latest,
35578
- releaseNotes: `${RELEASES}/v${latest}`,
35579
- upgrade: runLine(currentRunner(), latest, "upgrade")
35580
- };
35581
- }
35582
- }
35583
- var REGISTRY = "https://registry.npmjs.org/gdharness/latest", RELEASES = "https://github.com/Aureliolo/gdharness/releases/tag", CACHE_MS, REQUEST_TIMEOUT_MS = 1e4, MAX_BODY_BYTES, FIRST_RETRY_MS = 30000, MAX_RETRY_MS, VERSION;
35584
- var init_update_check = __esm(() => {
35585
- CACHE_MS = 4 * 60 * 60 * 1000;
35586
- MAX_BODY_BYTES = 1 << 20;
35587
- MAX_RETRY_MS = 60 * 60 * 1000;
35588
- VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
35589
- });
35590
-
35591
35636
  // src/server.ts
35592
35637
  import { execFile as execFile5, spawn } from "node:child_process";
35593
35638
  import { existsSync as existsSync10, mkdtempSync as mkdtempSync3, readdirSync as readdirSync6, readFileSync as readFileSync11, realpathSync as realpathSync2, rmSync as rmSync6 } from "node:fs";
@@ -36390,7 +36435,8 @@ class GodotServer {
36390
36435
  ];
36391
36436
  const timeoutMs = readPositiveNumber(args, "timeoutMs") ?? 600000;
36392
36437
  this.logDebug(`Running tests: ${engine.value} ${cmdArgs.join(" ")}`);
36393
- const run = this.spawnGame(engine.value, cmdArgs);
36438
+ const userData = mkdtempSync3(join12(tmpdir5(), "gdharness-tests-"));
36439
+ const run = this.spawnGame(engine.value, cmdArgs, userDataIn(userData));
36394
36440
  const hung = await new Promise((resolve) => {
36395
36441
  const timer = setTimeout(() => {
36396
36442
  run.process.kill();
@@ -36418,6 +36464,7 @@ class GodotServer {
36418
36464
  reportProblem = errorMessage(error);
36419
36465
  } finally {
36420
36466
  rmSync6(reportsDir, { recursive: true, force: true });
36467
+ rmSync6(userData, { recursive: true, force: true });
36421
36468
  }
36422
36469
  const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries;
36423
36470
  const verdicts = {
@@ -36528,7 +36575,7 @@ class GodotServer {
36528
36575
  addonIsStale: status.connected ? stale : undefined,
36529
36576
  bridgeAvailable: this.bridgeStartupError === null,
36530
36577
  startupError: this.bridgeStartupError,
36531
- staleNote: stale ? `The editor is running the ${status.addonVersion === "" ? "addon from before versions were reported" : status.addonVersion} addon while this server ships ${SERVER_VERSION}. Restart it with editor_launch restart to pick the new one up.` : undefined,
36578
+ staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
36532
36579
  note: isPortConflict ? "Bridge port is already in use. Another gdharness instance may own the editor bridge, so this server cannot report that editor connection." : undefined,
36533
36580
  suggestion: isPortConflict ? "Stop duplicate gdharness/MCP server instances or re-run the command from the same server process that owns the bridge port." : undefined
36534
36581
  };
@@ -36606,6 +36653,7 @@ class GodotServer {
36606
36653
  addonVersion: now.addonVersion,
36607
36654
  serverVersion: SERVER_VERSION,
36608
36655
  addonIsStale: now.addonVersion !== SERVER_VERSION,
36656
+ staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
36609
36657
  tookMs: Date.now() - began
36610
36658
  });
36611
36659
  }
@@ -36639,7 +36687,8 @@ class GodotServer {
36639
36687
  this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
36640
36688
  const editor = spawn(engine.value, editorArguments(project.value.path), {
36641
36689
  stdio: "ignore",
36642
- detached: true
36690
+ detached: true,
36691
+ env: { ...process.env, GDHARNESS_RUNTIME_DIR: runtimeDirectory() }
36643
36692
  });
36644
36693
  const started = await new Promise((resolve) => {
36645
36694
  editor.once("spawn", () => {
@@ -36772,8 +36821,8 @@ class GodotServer {
36772
36821
  }
36773
36822
  running.process?.kill();
36774
36823
  }
36775
- spawnGame(godotPath, cmdArgs) {
36776
- const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"] });
36824
+ spawnGame(godotPath, cmdArgs, env) {
36825
+ const child = spawn(godotPath, cmdArgs, { stdio: ["ignore", "pipe", "pipe"], ...env ? { env } : {} });
36777
36826
  const log = new GameLog;
36778
36827
  const started = {
36779
36828
  process: child,
@@ -38133,12 +38182,13 @@ class Ask {
38133
38182
  }
38134
38183
 
38135
38184
  // src/server-version.ts
38136
- import { readFileSync as readFileSync3 } from "node:fs";
38185
+ init_update_check();
38186
+ import { readFileSync as readFileSync4 } from "node:fs";
38137
38187
  var DEBUG_MODE2 = process.env["DEBUG"] === "true";
38138
38188
  var GODOT_DEBUG_MODE_DEFAULT2 = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE2;
38139
38189
  var SERVER_VERSION2 = (() => {
38140
38190
  try {
38141
- const pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
38191
+ const pkg = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8"));
38142
38192
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
38143
38193
  } catch {
38144
38194
  return "0.0.0";
@@ -38150,8 +38200,8 @@ init_class_cache();
38150
38200
  init_headless();
38151
38201
  init_resources();
38152
38202
  init_server_version();
38153
- import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
38154
- import { dirname as dirname2, join as join6 } from "node:path";
38203
+ import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
38204
+ import { dirname as dirname2, join as join7 } from "node:path";
38155
38205
  import { fileURLToPath } from "node:url";
38156
38206
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
38157
38207
  var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
@@ -38161,24 +38211,24 @@ var RUNTIME_AUTOLOAD = {
38161
38211
  };
38162
38212
  var VERSION_MARKER = ".gdharness-version";
38163
38213
  function shippedAddonsDirectory() {
38164
- return join6(dirname2(fileURLToPath(import.meta.url)), "godot", "addons");
38214
+ return join7(dirname2(fileURLToPath(import.meta.url)), "godot", "addons");
38165
38215
  }
38166
38216
  function shippedOperationsScript() {
38167
- return join6(dirname2(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
38217
+ return join7(dirname2(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
38168
38218
  }
38169
38219
  function installAddons(projectPath, from = shippedAddonsDirectory()) {
38170
38220
  const installed = [];
38171
38221
  for (const name of ADDONS) {
38172
- const source = join6(from, name);
38173
- if (!existsSync5(join6(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
38222
+ const source = join7(from, name);
38223
+ if (!existsSync6(join7(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
38174
38224
  throw new Error(`The package holds no ${name} addon at ${source}.`);
38175
38225
  }
38176
- const target = join6(projectPath, "addons", name);
38177
- const replaced = existsSync5(target);
38226
+ const target = join7(projectPath, "addons", name);
38227
+ const replaced = existsSync6(target);
38178
38228
  rmSync4(target, { recursive: true, force: true });
38179
- mkdirSync2(dirname2(target), { recursive: true });
38229
+ mkdirSync3(dirname2(target), { recursive: true });
38180
38230
  cpSync(source, target, { recursive: true });
38181
- writeFileSync4(join6(target, VERSION_MARKER), `${SERVER_VERSION}
38231
+ writeFileSync5(join7(target, VERSION_MARKER), `${SERVER_VERSION}
38182
38232
  `);
38183
38233
  installed.push({ name, path: target, replaced });
38184
38234
  }
@@ -38187,8 +38237,8 @@ function installAddons(projectPath, from = shippedAddonsDirectory()) {
38187
38237
  function removeAddons(projectPath) {
38188
38238
  const removed = [];
38189
38239
  for (const name of ADDONS) {
38190
- const target = join6(projectPath, "addons", name);
38191
- if (existsSync5(target)) {
38240
+ const target = join7(projectPath, "addons", name);
38241
+ if (existsSync6(target)) {
38192
38242
  rmSync4(target, { recursive: true, force: true });
38193
38243
  removed.push(target);
38194
38244
  }
@@ -38222,10 +38272,10 @@ function enabledPlugins(settings) {
38222
38272
  function inspectProject(projectPath) {
38223
38273
  const problems = [];
38224
38274
  const addons = ADDONS.map((name) => {
38225
- const directory = join6(projectPath, "addons", name);
38226
- const marker = join6(directory, VERSION_MARKER);
38227
- const installed = existsSync5(directory);
38228
- const version = existsSync5(marker) ? readFileSync6(marker, "utf8").trim() : null;
38275
+ const directory = join7(projectPath, "addons", name);
38276
+ const marker = join7(directory, VERSION_MARKER);
38277
+ const installed = existsSync6(directory);
38278
+ const version = existsSync6(marker) ? readFileSync7(marker, "utf8").trim() : null;
38229
38279
  const current = version === SERVER_VERSION;
38230
38280
  if (!installed) {
38231
38281
  problems.push(`addons/${name} is not installed; run gdharness setup`);
@@ -38234,7 +38284,7 @@ function inspectProject(projectPath) {
38234
38284
  }
38235
38285
  return { name, installed, version, current };
38236
38286
  });
38237
- const settings = parseProjectGodot(readFileSync6(join6(projectPath, "project.godot"), "utf8"));
38287
+ const settings = parseProjectGodot(readFileSync7(join7(projectPath, "project.godot"), "utf8"));
38238
38288
  const pluginsEnabled = enabledPlugins(settings);
38239
38289
  for (const name of EDITOR_PLUGINS) {
38240
38290
  if (!pluginsEnabled.includes(name)) {
@@ -38263,8 +38313,8 @@ function inspectProject(projectPath) {
38263
38313
 
38264
38314
  // src/skill.ts
38265
38315
  init_tool_definitions();
38266
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync3, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "node:fs";
38267
- import { basename, dirname as dirname3, join as join7 } from "node:path";
38316
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync3, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
38317
+ import { basename, dirname as dirname3, join as join8 } from "node:path";
38268
38318
 
38269
38319
  // src/tool-reference.ts
38270
38320
  init_tool_definitions();
@@ -38311,7 +38361,7 @@ function renderToolsMarkdown() {
38311
38361
 
38312
38362
  // src/skill.ts
38313
38363
  var SKILL_NAME = "gdharness";
38314
- var SHARED_SKILLS = join7(".agents", "skills");
38364
+ var SHARED_SKILLS = join8(".agents", "skills");
38315
38365
  function skillMarkdown(version) {
38316
38366
  return `---
38317
38367
  name: ${SKILL_NAME}
@@ -38333,7 +38383,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
38333
38383
  - Every call takes \`projectPath\`, except \`runtime_*\` and \`debug_*\`, where it picks between
38334
38384
  running games. There is no ambient project.
38335
38385
  - \`editor_status\` says whether an editor is connected and whether its addon matches the server.
38336
- \`addonIsStale\` means the editor is serving an older addon and needs restarting.
38386
+ \`addonIsStale\` means they differ, and \`staleNote\` says which half is behind: an editor that
38387
+ needs restarting, or a server that needs reconnecting in your harness.
38337
38388
  - Start the game with \`editor_run start\`, never by spawning an engine. The editor plays it, so
38338
38389
  its debugger holds it, which is what gives the \`debug_*\` tools something to talk to.
38339
38390
  - Read \`editor_output\` after every run. It returns the engine's errors and warnings as entries
@@ -38392,27 +38443,27 @@ whatever the engine said on stderr comes back under \`engine_messages\`.
38392
38443
  function skillFiles(version) {
38393
38444
  return new Map([
38394
38445
  ["SKILL.md", skillMarkdown(version)],
38395
- [join7("references", "tools.md"), renderToolsMarkdown()]
38446
+ [join8("references", "tools.md"), renderToolsMarkdown()]
38396
38447
  ]);
38397
38448
  }
38398
38449
  function skillDirectories(harnesses, projectPath, exists) {
38399
- const directories = [join7(projectPath, SHARED_SKILLS, SKILL_NAME)];
38450
+ const directories = [join8(projectPath, SHARED_SKILLS, SKILL_NAME)];
38400
38451
  for (const harness of harnesses) {
38401
38452
  if (harness.skills === undefined) {
38402
38453
  continue;
38403
38454
  }
38404
- const own = join7(projectPath, harness.skills.dir);
38405
- if ((!harness.skills.shared || exists(own)) && !directories.includes(join7(own, SKILL_NAME))) {
38406
- directories.push(join7(own, SKILL_NAME));
38455
+ const own = join8(projectPath, harness.skills.dir);
38456
+ if ((!harness.skills.shared || exists(own)) && !directories.includes(join8(own, SKILL_NAME))) {
38457
+ directories.push(join8(own, SKILL_NAME));
38407
38458
  }
38408
38459
  }
38409
38460
  return directories;
38410
38461
  }
38411
38462
  function everySkillDirectory(projectPath, harnesses) {
38412
- const directories = new Set([join7(projectPath, SHARED_SKILLS, SKILL_NAME)]);
38463
+ const directories = new Set([join8(projectPath, SHARED_SKILLS, SKILL_NAME)]);
38413
38464
  for (const harness of harnesses) {
38414
38465
  if (harness.skills !== undefined) {
38415
- directories.add(join7(projectPath, harness.skills.dir, SKILL_NAME));
38466
+ directories.add(join8(projectPath, harness.skills.dir, SKILL_NAME));
38416
38467
  }
38417
38468
  }
38418
38469
  return [...directories];
@@ -38420,7 +38471,7 @@ function everySkillDirectory(projectPath, harnesses) {
38420
38471
  function removeSkill(directories, projectPath) {
38421
38472
  const removed = [];
38422
38473
  for (const directory of directories) {
38423
- if (existsSync6(directory)) {
38474
+ if (existsSync7(directory)) {
38424
38475
  rmSync5(directory, { recursive: true, force: true });
38425
38476
  pruneEmpty(dirname3(directory), projectPath);
38426
38477
  removed.push(directory);
@@ -38429,7 +38480,7 @@ function removeSkill(directories, projectPath) {
38429
38480
  return removed;
38430
38481
  }
38431
38482
  function pruneEmpty(directory, projectPath) {
38432
- const shared = join7(projectPath, ".agents");
38483
+ const shared = join8(projectPath, ".agents");
38433
38484
  let path = directory;
38434
38485
  while (path !== projectPath && (basename(path) === "skills" || path === shared)) {
38435
38486
  if (readdirSync3(path).length > 0) {
@@ -38442,30 +38493,30 @@ function pruneEmpty(directory, projectPath) {
38442
38493
  function writeSkill(directories, version) {
38443
38494
  const files = skillFiles(version);
38444
38495
  return directories.map((directory) => {
38445
- const replaced = existsSync6(directory);
38496
+ const replaced = existsSync7(directory);
38446
38497
  rmSync5(directory, { recursive: true, force: true });
38447
38498
  for (const [name, contents] of files) {
38448
- const path = join7(directory, name);
38449
- mkdirSync3(dirname3(path), { recursive: true });
38450
- writeFileSync5(path, contents, "utf8");
38499
+ const path = join8(directory, name);
38500
+ mkdirSync4(dirname3(path), { recursive: true });
38501
+ writeFileSync6(path, contents, "utf8");
38451
38502
  }
38452
38503
  return { path: directory, replaced };
38453
38504
  });
38454
38505
  }
38455
38506
 
38456
38507
  // src/version.ts
38457
- import { readFileSync as readFileSync7 } from "node:fs";
38458
- import { dirname as dirname4, join as join8 } from "node:path";
38508
+ import { readFileSync as readFileSync8 } from "node:fs";
38509
+ import { dirname as dirname4, join as join9 } from "node:path";
38459
38510
  import { fileURLToPath as fileURLToPath2 } from "node:url";
38460
38511
  function getLocalVersion() {
38461
38512
  const currentDirectory = dirname4(fileURLToPath2(import.meta.url));
38462
38513
  const packageCandidates = [
38463
- join8(currentDirectory, "..", "package.json"),
38464
- join8(currentDirectory, "..", "..", "package.json")
38514
+ join9(currentDirectory, "..", "package.json"),
38515
+ join9(currentDirectory, "..", "..", "package.json")
38465
38516
  ];
38466
38517
  for (const packagePath of packageCandidates) {
38467
38518
  try {
38468
- const value = JSON.parse(readFileSync7(packagePath, "utf-8"));
38519
+ const value = JSON.parse(readFileSync8(packagePath, "utf-8"));
38469
38520
  if (typeof value === "object" && value !== null && "name" in value && value.name === "gdharness" && "version" in value && typeof value.version === "string") {
38470
38521
  return value.version;
38471
38522
  }