gdharness 0.5.0 → 0.5.1

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
@@ -7228,15 +7228,196 @@ var init_game_log = __esm(() => {
7228
7228
  COLOUR_CODE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
7229
7229
  });
7230
7230
 
7231
+ // src/update-check.ts
7232
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
7233
+ import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
7234
+ import { join as join4 } from "node:path";
7235
+ function cacheDirectory(environment) {
7236
+ const set = (name) => {
7237
+ const value = environment[name];
7238
+ return value !== undefined && value !== "" ? value : null;
7239
+ };
7240
+ const home = set("HOME") ?? homedir3();
7241
+ if (process.platform === "win32") {
7242
+ return join4(set("LOCALAPPDATA") ?? home, "gdharness");
7243
+ }
7244
+ if (process.platform === "darwin") {
7245
+ return join4(home, "Library", "Caches", "gdharness");
7246
+ }
7247
+ return join4(set("XDG_CACHE_HOME") ?? join4(home, ".cache"), "gdharness");
7248
+ }
7249
+ function cacheFile(environment = process.env) {
7250
+ try {
7251
+ const directory = cacheDirectory(environment);
7252
+ mkdirSync2(directory, { recursive: true, mode: 448 });
7253
+ return join4(directory, "update-check.json");
7254
+ } catch {
7255
+ return join4(tmpdir2(), "gdharness-update-check.json");
7256
+ }
7257
+ }
7258
+ function readCache(path) {
7259
+ try {
7260
+ if (!existsSync4(path)) {
7261
+ return null;
7262
+ }
7263
+ const parsed = JSON.parse(readFileSync2(path, "utf8"));
7264
+ if (typeof parsed !== "object" || parsed === null) {
7265
+ return null;
7266
+ }
7267
+ const record = parsed;
7268
+ const checkedAt = record["checkedAt"];
7269
+ const latest = record["latest"];
7270
+ if (typeof checkedAt !== "number" || typeof latest !== "string" || !VERSION.test(latest)) {
7271
+ return null;
7272
+ }
7273
+ return { checkedAt, latest };
7274
+ } catch {
7275
+ return null;
7276
+ }
7277
+ }
7278
+ function writeCache(path, entry) {
7279
+ try {
7280
+ writeFileSync3(path, JSON.stringify(entry), { encoding: "utf8", mode: 384 });
7281
+ } catch {}
7282
+ }
7283
+ function parts(version) {
7284
+ const withoutBuild = version.split("+")[0] ?? version;
7285
+ const dash = withoutBuild.indexOf("-");
7286
+ const numeric = dash === -1 ? withoutBuild : withoutBuild.slice(0, dash);
7287
+ return {
7288
+ numbers: numeric.split(".").map((piece) => Number.parseInt(piece, 10) || 0),
7289
+ prerelease: dash !== -1
7290
+ };
7291
+ }
7292
+ function isNewer(candidate, current) {
7293
+ const left = parts(candidate);
7294
+ const right = parts(current);
7295
+ for (let index = 0;index < 3; index += 1) {
7296
+ const a = left.numbers[index] ?? 0;
7297
+ const b = right.numbers[index] ?? 0;
7298
+ if (a !== b) {
7299
+ return a > b;
7300
+ }
7301
+ }
7302
+ return !left.prerelease && right.prerelease;
7303
+ }
7304
+ async function fetchLatest() {
7305
+ const response = await fetch(REGISTRY, {
7306
+ headers: { accept: "application/vnd.npm.install-v1+json, application/json" },
7307
+ redirect: "error",
7308
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
7309
+ });
7310
+ if (!response.ok || response.body === null) {
7311
+ return null;
7312
+ }
7313
+ const declared = Number.parseInt(response.headers.get("content-length") ?? "", 10);
7314
+ if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {
7315
+ return null;
7316
+ }
7317
+ const chunks = [];
7318
+ let size = 0;
7319
+ for await (const chunk of response.body) {
7320
+ size += chunk.byteLength;
7321
+ if (size > MAX_BODY_BYTES) {
7322
+ return null;
7323
+ }
7324
+ chunks.push(chunk);
7325
+ }
7326
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
7327
+ if (typeof parsed !== "object" || parsed === null) {
7328
+ return null;
7329
+ }
7330
+ const version = parsed["version"];
7331
+ return typeof version === "string" && VERSION.test(version) ? version : null;
7332
+ }
7333
+
7334
+ class UpdateCheck {
7335
+ latest = null;
7336
+ checking = false;
7337
+ checkedAt = 0;
7338
+ retryAt = 0;
7339
+ backoffMs = FIRST_RETRY_MS;
7340
+ enabled;
7341
+ current;
7342
+ cachePath;
7343
+ constructor(current, environment = process.env) {
7344
+ this.current = current;
7345
+ this.enabled = (environment["GDHARNESS_NO_UPDATE_CHECK"] ?? "") === "";
7346
+ this.cachePath = cacheFile(environment);
7347
+ const cached = this.enabled ? readCache(this.cachePath) : null;
7348
+ if (cached !== null) {
7349
+ this.latest = cached.latest;
7350
+ this.checkedAt = cached.checkedAt;
7351
+ }
7352
+ }
7353
+ refresh(now = Date.now()) {
7354
+ if (!this.enabled || this.checking || now < this.retryAt || now - this.checkedAt < CACHE_MS) {
7355
+ return;
7356
+ }
7357
+ this.checking = true;
7358
+ fetchLatest().then((version) => {
7359
+ if (version === null) {
7360
+ this.scheduleRetry(now);
7361
+ return;
7362
+ }
7363
+ this.latest = version;
7364
+ this.checkedAt = Date.now();
7365
+ this.backoffMs = FIRST_RETRY_MS;
7366
+ this.retryAt = 0;
7367
+ writeCache(this.cachePath, { checkedAt: this.checkedAt, latest: version });
7368
+ }).catch(() => {
7369
+ this.scheduleRetry(now);
7370
+ }).finally(() => {
7371
+ this.checking = false;
7372
+ });
7373
+ }
7374
+ scheduleRetry(now) {
7375
+ this.retryAt = now + this.backoffMs;
7376
+ this.backoffMs = Math.min(this.backoffMs * 2, MAX_RETRY_MS);
7377
+ }
7378
+ notice() {
7379
+ const latest = this.latest;
7380
+ if (!this.enabled || latest === null || !isNewer(latest, this.current)) {
7381
+ return null;
7382
+ }
7383
+ return {
7384
+ current: this.current,
7385
+ latest,
7386
+ releaseNotes: `${RELEASES}/v${latest}`,
7387
+ upgrade: runLine(currentRunner(), latest, "upgrade")
7388
+ };
7389
+ }
7390
+ }
7391
+ 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;
7392
+ var init_update_check = __esm(() => {
7393
+ CACHE_MS = 4 * 60 * 60 * 1000;
7394
+ MAX_BODY_BYTES = 1 << 20;
7395
+ MAX_RETRY_MS = 60 * 60 * 1000;
7396
+ VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
7397
+ });
7398
+
7231
7399
  // src/server-version.ts
7232
- import { readFileSync as readFileSync2 } from "node:fs";
7233
- var DEBUG_MODE, GODOT_DEBUG_MODE_DEFAULT, SERVER_VERSION;
7400
+ import { readFileSync as readFileSync3 } from "node:fs";
7401
+ function addonMismatch(addonVersion, serverVersion) {
7402
+ if (addonVersion === serverVersion) {
7403
+ return;
7404
+ }
7405
+ const reported = addonVersion ?? "";
7406
+ const editor = reported === "" ? UNVERSIONED : reported;
7407
+ const both = `The editor is running the ${editor} addon while this server ships ${serverVersion}.`;
7408
+ if (reported !== "" && isNewer(reported, serverVersion)) {
7409
+ return `${both} This server is the older half: reconnect it in your harness so it spawns ${reported}.`;
7410
+ }
7411
+ return `${both} Restart it with editor_launch restart to pick the new one up.`;
7412
+ }
7413
+ var DEBUG_MODE, GODOT_DEBUG_MODE_DEFAULT, SERVER_VERSION, UNVERSIONED = "addon from before versions were reported";
7234
7414
  var init_server_version = __esm(() => {
7415
+ init_update_check();
7235
7416
  DEBUG_MODE = process.env["DEBUG"] === "true";
7236
7417
  GODOT_DEBUG_MODE_DEFAULT = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE;
7237
7418
  SERVER_VERSION = (() => {
7238
7419
  try {
7239
- const pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
7420
+ const pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
7240
7421
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
7241
7422
  } catch {
7242
7423
  return "0.0.0";
@@ -7245,8 +7426,8 @@ var init_server_version = __esm(() => {
7245
7426
  });
7246
7427
 
7247
7428
  // 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";
7429
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
7430
+ import { join as join5 } from "node:path";
7250
7431
  function declaredClasses(projectPath) {
7251
7432
  const declared = new Map;
7252
7433
  const visit = (directory, prefix) => {
@@ -7254,11 +7435,11 @@ function declaredClasses(projectPath) {
7254
7435
  if (entry.name.startsWith(".")) {
7255
7436
  continue;
7256
7437
  }
7257
- const path = join4(directory, entry.name);
7438
+ const path = join5(directory, entry.name);
7258
7439
  if (entry.isDirectory()) {
7259
7440
  visit(path, `${prefix}${entry.name}/`);
7260
7441
  } 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"));
7442
+ const found = /^class_name\s+([A-Za-z_][A-Za-z0-9_]*)/m.exec(readFileSync5(path, "utf8"));
7262
7443
  if (found?.[1]) {
7263
7444
  declared.set(found[1], `res://${prefix}${entry.name}`);
7264
7445
  }
@@ -7269,12 +7450,12 @@ function declaredClasses(projectPath) {
7269
7450
  return declared;
7270
7451
  }
7271
7452
  function cachedClasses(projectPath) {
7272
- const cache = join4(projectPath, ".godot", "global_script_class_cache.cfg");
7273
- if (!existsSync4(cache)) {
7453
+ const cache = join5(projectPath, ".godot", "global_script_class_cache.cfg");
7454
+ if (!existsSync5(cache)) {
7274
7455
  return null;
7275
7456
  }
7276
7457
  const listed = new Map;
7277
- const text = readFileSync4(cache, "utf8");
7458
+ const text = readFileSync5(cache, "utf8");
7278
7459
  for (const entry of text.matchAll(/"class":\s*&"([^"]+)"[\s\S]*?"path":\s*"([^"]+)"/g)) {
7279
7460
  listed.set(entry[1] ?? "", entry[2] ?? "");
7280
7461
  }
@@ -7291,9 +7472,9 @@ var init_class_cache = () => {};
7291
7472
 
7292
7473
  // src/headless.ts
7293
7474
  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";
7475
+ import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
7476
+ import { tmpdir as tmpdir3 } from "node:os";
7477
+ import { join as join6 } from "node:path";
7297
7478
  import { promisify as promisify3 } from "node:util";
7298
7479
  function snakeCased2(params) {
7299
7480
  const result = emptyRecord();
@@ -7333,9 +7514,9 @@ function reason2(stdout, stderr) {
7333
7514
  return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
7334
7515
  }
7335
7516
  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");
7517
+ const paramsDir = mkdtempSync2(join6(tmpdir3(), "gdharness-params-"));
7518
+ const paramsFile = join6(paramsDir, `${operation}.json`);
7519
+ writeFileSync4(paramsFile, JSON.stringify(snakeCased2(params)), "utf8");
7339
7520
  const args = [
7340
7521
  "--headless",
7341
7522
  "--path",
@@ -14537,7 +14718,7 @@ var init_paths = __esm(() => {
14537
14718
  });
14538
14719
 
14539
14720
  // src/resources.ts
14540
- import { readFileSync as readFileSync5 } from "node:fs";
14721
+ import { readFileSync as readFileSync6 } from "node:fs";
14541
14722
  import { extname, resolve as resolve2 } from "node:path";
14542
14723
  function ensureProjectPath(getProjectPath) {
14543
14724
  const projectPath = getProjectPath();
@@ -14679,7 +14860,7 @@ function readResourceText(uri, getProjectPath) {
14679
14860
  const parsedUri = parseGodotUri(uri);
14680
14861
  if (parsedUri.kind === "project-info") {
14681
14862
  const projectFilePath = resolveProjectFile(projectPath, "project.godot");
14682
- const rawProject = readFileSync5(projectFilePath, "utf-8");
14863
+ const rawProject = readFileSync6(projectFilePath, "utf-8");
14683
14864
  const parsedProject = parseProjectGodot(rawProject);
14684
14865
  return {
14685
14866
  mimeType: "application/json",
@@ -14688,7 +14869,7 @@ function readResourceText(uri, getProjectPath) {
14688
14869
  }
14689
14870
  const filePath = resolveProjectFile(projectPath, parsedUri.resourcePath);
14690
14871
  ensureAllowedExtension(parsedUri.kind, filePath);
14691
- const text = readFileSync5(filePath, "utf-8");
14872
+ const text = readFileSync6(filePath, "utf-8");
14692
14873
  return {
14693
14874
  mimeType: parsedUri.kind === "script" ? "text/x-gdscript" : "text/plain",
14694
14875
  text
@@ -34297,7 +34478,7 @@ var init_godot_bridge = __esm(() => {
34297
34478
 
34298
34479
  // src/godot-path.ts
34299
34480
  import { execFile as execFile4 } from "node:child_process";
34300
- import { existsSync as existsSync7 } from "node:fs";
34481
+ import { existsSync as existsSync8 } from "node:fs";
34301
34482
  import { normalize as normalize2 } from "node:path";
34302
34483
  import { promisify as promisify4 } from "node:util";
34303
34484
  var run4, GodotLocator2;
@@ -34339,7 +34520,7 @@ var init_godot_path = __esm(() => {
34339
34520
  return known;
34340
34521
  }
34341
34522
  let ok = false;
34342
- if (path === "godot" || existsSync7(path)) {
34523
+ if (path === "godot" || existsSync8(path)) {
34343
34524
  try {
34344
34525
  await run4(path, ["--version"]);
34345
34526
  ok = true;
@@ -35142,8 +35323,8 @@ var init_lsp_client = __esm(() => {
35142
35323
  });
35143
35324
 
35144
35325
  // src/project-scan.ts
35145
- import { readdirSync as readdirSync4, readFileSync as readFileSync8 } from "node:fs";
35146
- import { join as join9 } from "node:path";
35326
+ import { readdirSync as readdirSync4, readFileSync as readFileSync9 } from "node:fs";
35327
+ import { join as join10 } from "node:path";
35147
35328
  function projectStructure(projectPath) {
35148
35329
  const structure = { scenes: 0, scripts: 0, assets: 0, other: 0 };
35149
35330
  const visit = (directory) => {
@@ -35152,7 +35333,7 @@ function projectStructure(projectPath) {
35152
35333
  continue;
35153
35334
  }
35154
35335
  if (entry.isDirectory()) {
35155
- visit(join9(directory, entry.name));
35336
+ visit(join10(directory, entry.name));
35156
35337
  } else if (entry.isFile()) {
35157
35338
  const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
35158
35339
  if (extension === "tscn") {
@@ -35194,7 +35375,7 @@ function searchProject(projectPath, options) {
35194
35375
  if (SKIPPED.has(entry.name)) {
35195
35376
  continue;
35196
35377
  }
35197
- const entryPath = join9(directory, entry.name);
35378
+ const entryPath = join10(directory, entry.name);
35198
35379
  if (entry.isDirectory()) {
35199
35380
  visit(entryPath);
35200
35381
  continue;
@@ -35205,7 +35386,7 @@ function searchProject(projectPath, options) {
35205
35386
  }
35206
35387
  result.summary.files_searched += 1;
35207
35388
  const matches = [];
35208
- for (const [index, line] of readFileSync8(entryPath, "utf8").split(`
35389
+ for (const [index, line] of readFileSync9(entryPath, "utf8").split(`
35209
35390
  `).entries()) {
35210
35391
  if (full()) {
35211
35392
  break;
@@ -35233,17 +35414,25 @@ var init_project_scan = __esm(() => {
35233
35414
  });
35234
35415
 
35235
35416
  // src/runtime-client.ts
35236
- import { existsSync as existsSync8, readdirSync as readdirSync5, readFileSync as readFileSync9, unlinkSync } from "node:fs";
35417
+ import { existsSync as existsSync9, readdirSync as readdirSync5, readFileSync as readFileSync10, unlinkSync } from "node:fs";
35237
35418
  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";
35419
+ import { tmpdir as tmpdir4 } from "node:os";
35420
+ import { join as join11, resolve as resolve4 } from "node:path";
35240
35421
  function runtimeDirectory(variables = process.env) {
35241
35422
  const explicit = envValue("GDHARNESS_RUNTIME_DIR", variables);
35242
35423
  if (explicit) {
35243
35424
  return explicit;
35244
35425
  }
35245
35426
  const perUser = envValue("XDG_RUNTIME_DIR", variables);
35246
- return join10(perUser ?? tmpdir3(), "gdharness");
35427
+ return join11(perUser ?? tmpdir4(), "gdharness");
35428
+ }
35429
+ function runtimeDirectories(variables = process.env) {
35430
+ const candidates = [runtimeDirectory(variables)];
35431
+ const fallbacks = process.platform === "win32" ? [envValue("SystemRoot", variables) ?? envValue("windir", variables) ?? "C:\\Windows"] : ["/tmp", "/var/tmp"];
35432
+ for (const base of fallbacks) {
35433
+ candidates.push(join11(base, process.platform === "win32" ? "Temp" : "", "gdharness"));
35434
+ }
35435
+ return [...new Set(candidates.map((path) => resolve4(path)))];
35247
35436
  }
35248
35437
  function processAlive(pid) {
35249
35438
  try {
@@ -35256,7 +35445,7 @@ function processAlive(pid) {
35256
35445
  function parseAnnouncement(file, pid) {
35257
35446
  let fields;
35258
35447
  try {
35259
- fields = asParams(JSON.parse(readFileSync9(file, "utf8")));
35448
+ fields = asParams(JSON.parse(readFileSync10(file, "utf8")));
35260
35449
  } catch {
35261
35450
  return null;
35262
35451
  }
@@ -35274,8 +35463,19 @@ function parseAnnouncement(file, pid) {
35274
35463
  file
35275
35464
  };
35276
35465
  }
35277
- function discoverRuntimes(directory = runtimeDirectory()) {
35278
- if (!existsSync8(directory)) {
35466
+ function discoverRuntimes(directories = runtimeDirectories()) {
35467
+ const found = new Map;
35468
+ for (const directory of directories) {
35469
+ for (const endpoint of announcedIn(directory)) {
35470
+ if (!found.has(endpoint.pid)) {
35471
+ found.set(endpoint.pid, endpoint);
35472
+ }
35473
+ }
35474
+ }
35475
+ return [...found.values()].sort((a, b) => b.pid - a.pid);
35476
+ }
35477
+ function announcedIn(directory) {
35478
+ if (!existsSync9(directory)) {
35279
35479
  return [];
35280
35480
  }
35281
35481
  const found = [];
@@ -35284,7 +35484,7 @@ function discoverRuntimes(directory = runtimeDirectory()) {
35284
35484
  if (!match) {
35285
35485
  continue;
35286
35486
  }
35287
- const file = join10(directory, entry);
35487
+ const file = join11(directory, entry);
35288
35488
  const pid = Number.parseInt(match[1] ?? "", 10);
35289
35489
  const endpoint = processAlive(pid) ? parseAnnouncement(file, pid) : null;
35290
35490
  if (endpoint) {
@@ -35295,7 +35495,7 @@ function discoverRuntimes(directory = runtimeDirectory()) {
35295
35495
  } catch {}
35296
35496
  }
35297
35497
  }
35298
- return found.sort((a, b) => b.pid - a.pid);
35498
+ return found;
35299
35499
  }
35300
35500
  function describe2(endpoint) {
35301
35501
  return `pid ${endpoint.pid} on ${endpoint.address}:${endpoint.port} (${endpoint.project.name || "unnamed"} at ${endpoint.project.path})`;
@@ -35420,174 +35620,6 @@ var init_runtime_client = __esm(() => {
35420
35620
  ANNOUNCEMENT_PATTERN = /^runtime-(\d+)\.json$/;
35421
35621
  });
35422
35622
 
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
35623
  // src/server.ts
35592
35624
  import { execFile as execFile5, spawn } from "node:child_process";
35593
35625
  import { existsSync as existsSync10, mkdtempSync as mkdtempSync3, readdirSync as readdirSync6, readFileSync as readFileSync11, realpathSync as realpathSync2, rmSync as rmSync6 } from "node:fs";
@@ -36528,7 +36560,7 @@ class GodotServer {
36528
36560
  addonIsStale: status.connected ? stale : undefined,
36529
36561
  bridgeAvailable: this.bridgeStartupError === null,
36530
36562
  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,
36563
+ staleNote: stale ? addonMismatch(status.addonVersion, SERVER_VERSION) : undefined,
36532
36564
  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
36565
  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
36566
  };
@@ -36606,6 +36638,7 @@ class GodotServer {
36606
36638
  addonVersion: now.addonVersion,
36607
36639
  serverVersion: SERVER_VERSION,
36608
36640
  addonIsStale: now.addonVersion !== SERVER_VERSION,
36641
+ staleNote: addonMismatch(now.addonVersion, SERVER_VERSION),
36609
36642
  tookMs: Date.now() - began
36610
36643
  });
36611
36644
  }
@@ -36639,7 +36672,8 @@ class GodotServer {
36639
36672
  this.logDebug(`Launching Godot editor for project: ${project.value.path}`);
36640
36673
  const editor = spawn(engine.value, editorArguments(project.value.path), {
36641
36674
  stdio: "ignore",
36642
- detached: true
36675
+ detached: true,
36676
+ env: { ...process.env, GDHARNESS_RUNTIME_DIR: runtimeDirectory() }
36643
36677
  });
36644
36678
  const started = await new Promise((resolve) => {
36645
36679
  editor.once("spawn", () => {
@@ -38133,12 +38167,13 @@ class Ask {
38133
38167
  }
38134
38168
 
38135
38169
  // src/server-version.ts
38136
- import { readFileSync as readFileSync3 } from "node:fs";
38170
+ init_update_check();
38171
+ import { readFileSync as readFileSync4 } from "node:fs";
38137
38172
  var DEBUG_MODE2 = process.env["DEBUG"] === "true";
38138
38173
  var GODOT_DEBUG_MODE_DEFAULT2 = process.env["GODOT_DEBUG"] === "true" || DEBUG_MODE2;
38139
38174
  var SERVER_VERSION2 = (() => {
38140
38175
  try {
38141
- const pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
38176
+ const pkg = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8"));
38142
38177
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
38143
38178
  } catch {
38144
38179
  return "0.0.0";
@@ -38150,8 +38185,8 @@ init_class_cache();
38150
38185
  init_headless();
38151
38186
  init_resources();
38152
38187
  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";
38188
+ import { cpSync, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
38189
+ import { dirname as dirname2, join as join7 } from "node:path";
38155
38190
  import { fileURLToPath } from "node:url";
38156
38191
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
38157
38192
  var EDITOR_PLUGINS = ["gdharness_editor", "auto_reload"];
@@ -38161,24 +38196,24 @@ var RUNTIME_AUTOLOAD = {
38161
38196
  };
38162
38197
  var VERSION_MARKER = ".gdharness-version";
38163
38198
  function shippedAddonsDirectory() {
38164
- return join6(dirname2(fileURLToPath(import.meta.url)), "godot", "addons");
38199
+ return join7(dirname2(fileURLToPath(import.meta.url)), "godot", "addons");
38165
38200
  }
38166
38201
  function shippedOperationsScript() {
38167
- return join6(dirname2(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
38202
+ return join7(dirname2(fileURLToPath(import.meta.url)), "godot", "operations", "godot_operations.gd");
38168
38203
  }
38169
38204
  function installAddons(projectPath, from = shippedAddonsDirectory()) {
38170
38205
  const installed = [];
38171
38206
  for (const name of ADDONS) {
38172
- const source = join6(from, name);
38173
- if (!existsSync5(join6(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
38207
+ const source = join7(from, name);
38208
+ if (!existsSync6(join7(source, name === "gdharness_runtime" ? "runtime_autoload.gd" : "plugin.cfg"))) {
38174
38209
  throw new Error(`The package holds no ${name} addon at ${source}.`);
38175
38210
  }
38176
- const target = join6(projectPath, "addons", name);
38177
- const replaced = existsSync5(target);
38211
+ const target = join7(projectPath, "addons", name);
38212
+ const replaced = existsSync6(target);
38178
38213
  rmSync4(target, { recursive: true, force: true });
38179
- mkdirSync2(dirname2(target), { recursive: true });
38214
+ mkdirSync3(dirname2(target), { recursive: true });
38180
38215
  cpSync(source, target, { recursive: true });
38181
- writeFileSync4(join6(target, VERSION_MARKER), `${SERVER_VERSION}
38216
+ writeFileSync5(join7(target, VERSION_MARKER), `${SERVER_VERSION}
38182
38217
  `);
38183
38218
  installed.push({ name, path: target, replaced });
38184
38219
  }
@@ -38187,8 +38222,8 @@ function installAddons(projectPath, from = shippedAddonsDirectory()) {
38187
38222
  function removeAddons(projectPath) {
38188
38223
  const removed = [];
38189
38224
  for (const name of ADDONS) {
38190
- const target = join6(projectPath, "addons", name);
38191
- if (existsSync5(target)) {
38225
+ const target = join7(projectPath, "addons", name);
38226
+ if (existsSync6(target)) {
38192
38227
  rmSync4(target, { recursive: true, force: true });
38193
38228
  removed.push(target);
38194
38229
  }
@@ -38222,10 +38257,10 @@ function enabledPlugins(settings) {
38222
38257
  function inspectProject(projectPath) {
38223
38258
  const problems = [];
38224
38259
  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;
38260
+ const directory = join7(projectPath, "addons", name);
38261
+ const marker = join7(directory, VERSION_MARKER);
38262
+ const installed = existsSync6(directory);
38263
+ const version = existsSync6(marker) ? readFileSync7(marker, "utf8").trim() : null;
38229
38264
  const current = version === SERVER_VERSION;
38230
38265
  if (!installed) {
38231
38266
  problems.push(`addons/${name} is not installed; run gdharness setup`);
@@ -38234,7 +38269,7 @@ function inspectProject(projectPath) {
38234
38269
  }
38235
38270
  return { name, installed, version, current };
38236
38271
  });
38237
- const settings = parseProjectGodot(readFileSync6(join6(projectPath, "project.godot"), "utf8"));
38272
+ const settings = parseProjectGodot(readFileSync7(join7(projectPath, "project.godot"), "utf8"));
38238
38273
  const pluginsEnabled = enabledPlugins(settings);
38239
38274
  for (const name of EDITOR_PLUGINS) {
38240
38275
  if (!pluginsEnabled.includes(name)) {
@@ -38263,8 +38298,8 @@ function inspectProject(projectPath) {
38263
38298
 
38264
38299
  // src/skill.ts
38265
38300
  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";
38301
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readdirSync as readdirSync3, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
38302
+ import { basename, dirname as dirname3, join as join8 } from "node:path";
38268
38303
 
38269
38304
  // src/tool-reference.ts
38270
38305
  init_tool_definitions();
@@ -38311,7 +38346,7 @@ function renderToolsMarkdown() {
38311
38346
 
38312
38347
  // src/skill.ts
38313
38348
  var SKILL_NAME = "gdharness";
38314
- var SHARED_SKILLS = join7(".agents", "skills");
38349
+ var SHARED_SKILLS = join8(".agents", "skills");
38315
38350
  function skillMarkdown(version) {
38316
38351
  return `---
38317
38352
  name: ${SKILL_NAME}
@@ -38333,7 +38368,8 @@ that is running, and the project on disk. ${TOOL_SPECS.length} tools, named \`do
38333
38368
  - Every call takes \`projectPath\`, except \`runtime_*\` and \`debug_*\`, where it picks between
38334
38369
  running games. There is no ambient project.
38335
38370
  - \`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.
38371
+ \`addonIsStale\` means they differ, and \`staleNote\` says which half is behind: an editor that
38372
+ needs restarting, or a server that needs reconnecting in your harness.
38337
38373
  - Start the game with \`editor_run start\`, never by spawning an engine. The editor plays it, so
38338
38374
  its debugger holds it, which is what gives the \`debug_*\` tools something to talk to.
38339
38375
  - Read \`editor_output\` after every run. It returns the engine's errors and warnings as entries
@@ -38392,27 +38428,27 @@ whatever the engine said on stderr comes back under \`engine_messages\`.
38392
38428
  function skillFiles(version) {
38393
38429
  return new Map([
38394
38430
  ["SKILL.md", skillMarkdown(version)],
38395
- [join7("references", "tools.md"), renderToolsMarkdown()]
38431
+ [join8("references", "tools.md"), renderToolsMarkdown()]
38396
38432
  ]);
38397
38433
  }
38398
38434
  function skillDirectories(harnesses, projectPath, exists) {
38399
- const directories = [join7(projectPath, SHARED_SKILLS, SKILL_NAME)];
38435
+ const directories = [join8(projectPath, SHARED_SKILLS, SKILL_NAME)];
38400
38436
  for (const harness of harnesses) {
38401
38437
  if (harness.skills === undefined) {
38402
38438
  continue;
38403
38439
  }
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));
38440
+ const own = join8(projectPath, harness.skills.dir);
38441
+ if ((!harness.skills.shared || exists(own)) && !directories.includes(join8(own, SKILL_NAME))) {
38442
+ directories.push(join8(own, SKILL_NAME));
38407
38443
  }
38408
38444
  }
38409
38445
  return directories;
38410
38446
  }
38411
38447
  function everySkillDirectory(projectPath, harnesses) {
38412
- const directories = new Set([join7(projectPath, SHARED_SKILLS, SKILL_NAME)]);
38448
+ const directories = new Set([join8(projectPath, SHARED_SKILLS, SKILL_NAME)]);
38413
38449
  for (const harness of harnesses) {
38414
38450
  if (harness.skills !== undefined) {
38415
- directories.add(join7(projectPath, harness.skills.dir, SKILL_NAME));
38451
+ directories.add(join8(projectPath, harness.skills.dir, SKILL_NAME));
38416
38452
  }
38417
38453
  }
38418
38454
  return [...directories];
@@ -38420,7 +38456,7 @@ function everySkillDirectory(projectPath, harnesses) {
38420
38456
  function removeSkill(directories, projectPath) {
38421
38457
  const removed = [];
38422
38458
  for (const directory of directories) {
38423
- if (existsSync6(directory)) {
38459
+ if (existsSync7(directory)) {
38424
38460
  rmSync5(directory, { recursive: true, force: true });
38425
38461
  pruneEmpty(dirname3(directory), projectPath);
38426
38462
  removed.push(directory);
@@ -38429,7 +38465,7 @@ function removeSkill(directories, projectPath) {
38429
38465
  return removed;
38430
38466
  }
38431
38467
  function pruneEmpty(directory, projectPath) {
38432
- const shared = join7(projectPath, ".agents");
38468
+ const shared = join8(projectPath, ".agents");
38433
38469
  let path = directory;
38434
38470
  while (path !== projectPath && (basename(path) === "skills" || path === shared)) {
38435
38471
  if (readdirSync3(path).length > 0) {
@@ -38442,30 +38478,30 @@ function pruneEmpty(directory, projectPath) {
38442
38478
  function writeSkill(directories, version) {
38443
38479
  const files = skillFiles(version);
38444
38480
  return directories.map((directory) => {
38445
- const replaced = existsSync6(directory);
38481
+ const replaced = existsSync7(directory);
38446
38482
  rmSync5(directory, { recursive: true, force: true });
38447
38483
  for (const [name, contents] of files) {
38448
- const path = join7(directory, name);
38449
- mkdirSync3(dirname3(path), { recursive: true });
38450
- writeFileSync5(path, contents, "utf8");
38484
+ const path = join8(directory, name);
38485
+ mkdirSync4(dirname3(path), { recursive: true });
38486
+ writeFileSync6(path, contents, "utf8");
38451
38487
  }
38452
38488
  return { path: directory, replaced };
38453
38489
  });
38454
38490
  }
38455
38491
 
38456
38492
  // src/version.ts
38457
- import { readFileSync as readFileSync7 } from "node:fs";
38458
- import { dirname as dirname4, join as join8 } from "node:path";
38493
+ import { readFileSync as readFileSync8 } from "node:fs";
38494
+ import { dirname as dirname4, join as join9 } from "node:path";
38459
38495
  import { fileURLToPath as fileURLToPath2 } from "node:url";
38460
38496
  function getLocalVersion() {
38461
38497
  const currentDirectory = dirname4(fileURLToPath2(import.meta.url));
38462
38498
  const packageCandidates = [
38463
- join8(currentDirectory, "..", "package.json"),
38464
- join8(currentDirectory, "..", "..", "package.json")
38499
+ join9(currentDirectory, "..", "package.json"),
38500
+ join9(currentDirectory, "..", "..", "package.json")
38465
38501
  ];
38466
38502
  for (const packagePath of packageCandidates) {
38467
38503
  try {
38468
- const value = JSON.parse(readFileSync7(packagePath, "utf-8"));
38504
+ const value = JSON.parse(readFileSync8(packagePath, "utf-8"));
38469
38505
  if (typeof value === "object" && value !== null && "name" in value && value.name === "gdharness" && "version" in value && typeof value.version === "string") {
38470
38506
  return value.version;
38471
38507
  }