gdharness 0.13.11 → 0.13.13

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
@@ -7281,6 +7281,32 @@ var init_game_log = __esm(() => {
7281
7281
  STREAMS = ["stdout", "stderr", "transcript"];
7282
7282
  });
7283
7283
 
7284
+ // src/scratch.ts
7285
+ import { rmSync as rmSync2 } from "node:fs";
7286
+ function discardWith(remove, paths) {
7287
+ const left = [];
7288
+ for (const path of paths) {
7289
+ if (!path) {
7290
+ continue;
7291
+ }
7292
+ try {
7293
+ remove(path);
7294
+ } catch (error) {
7295
+ left.push({ path, reason: error instanceof Error ? error.message : String(error) });
7296
+ }
7297
+ }
7298
+ return left;
7299
+ }
7300
+ function discard(...paths) {
7301
+ return discardWith((path) => {
7302
+ rmSync2(path, SWEPT);
7303
+ }, paths);
7304
+ }
7305
+ var SWEPT;
7306
+ var init_scratch = __esm(() => {
7307
+ SWEPT = { recursive: true, force: true, maxRetries: 20, retryDelay: 250 };
7308
+ });
7309
+
7284
7310
  // src/update-check.ts
7285
7311
  import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs";
7286
7312
  import { homedir as homedir3, tmpdir as tmpdir2 } from "node:os";
@@ -7554,7 +7580,7 @@ var init_class_cache = () => {};
7554
7580
 
7555
7581
  // src/headless.ts
7556
7582
  import { execFile as execFile3 } from "node:child_process";
7557
- import { mkdtempSync as mkdtempSync2, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
7583
+ import { mkdtempSync as mkdtempSync2, writeFileSync as writeFileSync4 } from "node:fs";
7558
7584
  import { tmpdir as tmpdir3 } from "node:os";
7559
7585
  import { join as join7 } from "node:path";
7560
7586
  import { promisify as promisify3 } from "node:util";
@@ -7595,6 +7621,36 @@ function reason2(stdout, stderr) {
7595
7621
  }
7596
7622
  return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
7597
7623
  }
7624
+ async function runImport(godotPath, projectPath) {
7625
+ const logDir = mkdtempSync2(join7(tmpdir3(), "gdharness-import-"));
7626
+ try {
7627
+ const { stderr } = await run3(godotPath, [
7628
+ "--headless",
7629
+ "--log-file",
7630
+ join7(logDir, "engine.log"),
7631
+ "--path",
7632
+ projectPath,
7633
+ "--import"
7634
+ ]);
7635
+ return { ok: true, messages: problems2(stderr) };
7636
+ } catch (error) {
7637
+ if (error instanceof Error && "stdout" in error && "stderr" in error) {
7638
+ const failed = error;
7639
+ return {
7640
+ ok: false,
7641
+ message: `the import pass failed (exit ${failed.code ?? "unknown"}): ${reason2(failed.stdout, failed.stderr)}`,
7642
+ messages: problems2(failed.stderr)
7643
+ };
7644
+ }
7645
+ return {
7646
+ ok: false,
7647
+ message: `the import pass could not be run: ${error instanceof Error ? error.message : String(error)}`,
7648
+ messages: []
7649
+ };
7650
+ } finally {
7651
+ discard(logDir);
7652
+ }
7653
+ }
7598
7654
  async function runOperation2(engine, operation, params, projectPath) {
7599
7655
  const paramsDir = mkdtempSync2(join7(tmpdir3(), "gdharness-params-"));
7600
7656
  const paramsFile = join7(paramsDir, `${operation}.json`);
@@ -7630,7 +7686,7 @@ async function runOperation2(engine, operation, params, projectPath) {
7630
7686
  messages: []
7631
7687
  };
7632
7688
  } finally {
7633
- rmSync3(paramsDir, { recursive: true, force: true });
7689
+ discard(paramsDir);
7634
7690
  }
7635
7691
  const payload = lastJsonObject2(stdout);
7636
7692
  if (payload === null) {
@@ -7645,6 +7701,7 @@ async function runOperation2(engine, operation, params, projectPath) {
7645
7701
  var run3;
7646
7702
  var init_headless = __esm(() => {
7647
7703
  init_game_log();
7704
+ init_scratch();
7648
7705
  run3 = promisify3(execFile3);
7649
7706
  });
7650
7707
 
@@ -15448,7 +15505,10 @@ var init_tool_definitions = __esm(() => {
15448
15505
  requires: []
15449
15506
  },
15450
15507
  uid: { summary: "the UID of one file", requires: ["resourcePath"] },
15451
- refresh_uids: { summary: "resave every resource so UID references are current", requires: [] },
15508
+ refresh_uids: {
15509
+ summary: "import the project so every script and shader has its .uid sidecar, and name under uidsCreated the ones this made and under stillWithoutUid the ones the engine would not import: it writes no scene and no script, so a project whose sidecars are all present is left untouched",
15510
+ requires: []
15511
+ },
15452
15512
  refresh_classes: {
15453
15513
  summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong',
15454
15514
  requires: []
@@ -16196,7 +16256,7 @@ var init_tool_definitions = __esm(() => {
16196
16256
  });
16197
16257
 
16198
16258
  // src/headless-operations.ts
16199
- var HEADLESS_OPERATIONS, EDITOR_READS;
16259
+ var HEADLESS_OPERATIONS, ENGINE_PASSES, EDITOR_READS;
16200
16260
  var init_headless_operations = __esm(() => {
16201
16261
  HEADLESS_OPERATIONS = dictionary({
16202
16262
  project_settings: dictionary({
@@ -16218,7 +16278,6 @@ var init_headless_operations = __esm(() => {
16218
16278
  set_options: "set_import_options",
16219
16279
  reimport: "reimport_resource",
16220
16280
  uid: "get_uid",
16221
- refresh_uids: "resave_resources",
16222
16281
  refresh_classes: "refresh_class_cache"
16223
16282
  }),
16224
16283
  project_export: dictionary({ list: "list_export_presets" }),
@@ -16230,6 +16289,9 @@ var init_headless_operations = __esm(() => {
16230
16289
  inheritance: "inspect_inheritance"
16231
16290
  })
16232
16291
  });
16292
+ ENGINE_PASSES = dictionary({
16293
+ project_import: dictionary({ refresh_uids: "import" })
16294
+ });
16233
16295
  EDITOR_READS = dictionary({
16234
16296
  project_settings: dictionary({ get: "get_project_setting" })
16235
16297
  });
@@ -30946,7 +31008,7 @@ var init_stdio2 = __esm(() => {
30946
31008
  });
30947
31009
 
30948
31010
  // src/bridge-announce.ts
30949
- import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "node:fs";
31011
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync9, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "node:fs";
30950
31012
  import { dirname as dirname6, join as join11 } from "node:path";
30951
31013
  function announcementPath(projectPath) {
30952
31014
  return join11(projectPath, ".godot", "gdharness-bridge.json");
@@ -30994,7 +31056,7 @@ function withdrawBridge(path) {
30994
31056
  return;
30995
31057
  }
30996
31058
  try {
30997
- rmSync6(path, { force: true });
31059
+ rmSync5(path, { force: true });
30998
31060
  } catch (error) {
30999
31061
  console.error(`[SERVER] Could not withdraw the editor bridge announcement: ${errorMessage(error)}`);
31000
31062
  }
@@ -36022,6 +36084,33 @@ function projectStructure(projectPath) {
36022
36084
  visit(projectPath);
36023
36085
  return structure;
36024
36086
  }
36087
+ function scriptsWithoutUid(projectPath) {
36088
+ const found = [];
36089
+ const visit = (directory, prefix) => {
36090
+ if (steppedOver(directory)) {
36091
+ return;
36092
+ }
36093
+ for (const entry of readdirSync4(directory, { withFileTypes: true })) {
36094
+ if (skipped(entry.name)) {
36095
+ continue;
36096
+ }
36097
+ const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
36098
+ if (entry.isDirectory()) {
36099
+ visit(join13(directory, entry.name), spelled);
36100
+ continue;
36101
+ }
36102
+ if (!entry.isFile()) {
36103
+ continue;
36104
+ }
36105
+ const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
36106
+ if (CARRIES_A_UID.has(extension) && !existsSync11(join13(directory, `${entry.name}.uid`))) {
36107
+ found.push(spelled);
36108
+ }
36109
+ }
36110
+ };
36111
+ visit(projectPath, "");
36112
+ return found.sort();
36113
+ }
36025
36114
  function searchProject(projectPath, options) {
36026
36115
  const extensions = new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
36027
36116
  const result = {
@@ -36081,10 +36170,11 @@ function searchProject(projectPath, options) {
36081
36170
  visit(projectPath);
36082
36171
  return result;
36083
36172
  }
36084
- var SKIPPED, ASSET_EXTENSIONS;
36173
+ var SKIPPED, ASSET_EXTENSIONS, CARRIES_A_UID;
36085
36174
  var init_project_scan = __esm(() => {
36086
36175
  SKIPPED = new Set(["node_modules"]);
36087
36176
  ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
36177
+ CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
36088
36178
  });
36089
36179
 
36090
36180
  // src/runtime-client.ts
@@ -36330,7 +36420,7 @@ var init_runtime_client = __esm(() => {
36330
36420
 
36331
36421
  // src/run-record.ts
36332
36422
  import { execFileSync } from "node:child_process";
36333
- import { mkdirSync as mkdirSync6, openSync, readdirSync as readdirSync6, readFileSync as readFileSync13, rmSync as rmSync7, statSync as statSync3, writeFileSync as writeFileSync8 } from "node:fs";
36423
+ import { mkdirSync as mkdirSync6, openSync, readdirSync as readdirSync6, readFileSync as readFileSync13, rmSync as rmSync6, statSync as statSync3, writeFileSync as writeFileSync8 } from "node:fs";
36334
36424
  import { basename as basename2, join as join15 } from "node:path";
36335
36425
  function runsDirectory() {
36336
36426
  return join15(runtimeDirectory(), "runs");
@@ -36540,7 +36630,7 @@ function clearRunRecord(ours = () => true) {
36540
36630
  continue;
36541
36631
  }
36542
36632
  try {
36543
- rmSync7(path);
36633
+ rmSync6(path);
36544
36634
  } catch {}
36545
36635
  }
36546
36636
  }
@@ -36569,7 +36659,7 @@ function sweepTranscripts(keepMs = 24 * 60 * 60 * 1000, now = Date.now()) {
36569
36659
  continue;
36570
36660
  }
36571
36661
  try {
36572
- rmSync7(path);
36662
+ rmSync6(path);
36573
36663
  } catch {}
36574
36664
  }
36575
36665
  }
@@ -36578,7 +36668,7 @@ var init_run_record = __esm(() => {
36578
36668
  });
36579
36669
 
36580
36670
  // src/setup.ts
36581
- import { cpSync as cpSync2, existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync14, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "node:fs";
36671
+ import { cpSync as cpSync2, existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync14, rmSync as rmSync7, writeFileSync as writeFileSync9 } from "node:fs";
36582
36672
  import { dirname as dirname8, join as join16 } from "node:path";
36583
36673
  function installedAddonVersion(projectPath) {
36584
36674
  const marker = join16(projectPath, "addons", ADDONS2[0], VERSION_MARKER2);
@@ -36614,8 +36704,7 @@ import {
36614
36704
  openSync as openSync2,
36615
36705
  readdirSync as readdirSync7,
36616
36706
  readFileSync as readFileSync15,
36617
- readSync,
36618
- rmSync as rmSync9
36707
+ readSync
36619
36708
  } from "node:fs";
36620
36709
  import { tmpdir as tmpdir5 } from "node:os";
36621
36710
  import { basename as basename3, dirname as dirname9, join as join17, normalize as normalize3 } from "node:path";
@@ -37192,6 +37281,9 @@ class GodotServer {
37192
37281
  return { ok: true, op };
37193
37282
  }
37194
37283
  async dispatch(tool, op, args) {
37284
+ if (ENGINE_PASSES[tool]?.[op] !== undefined) {
37285
+ return await this.handleRefreshUids(args);
37286
+ }
37195
37287
  const headless = HEADLESS_OPERATIONS[tool]?.[op];
37196
37288
  if (headless !== undefined) {
37197
37289
  const { from: asked, ...answerable } = args;
@@ -37562,6 +37654,33 @@ class GodotServer {
37562
37654
  const { op: _op, projectPath: _projectPath, ...params } = contained.value;
37563
37655
  return this.answer(await this.operation(operation, params, project.value.path));
37564
37656
  }
37657
+ async handleRefreshUids(args) {
37658
+ const project = this.project(args);
37659
+ if (!project.ok) {
37660
+ return project.response;
37661
+ }
37662
+ const engine = await this.engine();
37663
+ if (!engine.ok) {
37664
+ return this.createErrorResponse("No Godot executable found.");
37665
+ }
37666
+ const projectPath = project.value.path;
37667
+ const before = scriptsWithoutUid(projectPath);
37668
+ const imported = await runImport(engine.value, projectPath);
37669
+ if (!imported.ok) {
37670
+ return this.answer(imported);
37671
+ }
37672
+ const after = scriptsWithoutUid(projectPath);
37673
+ const given = before.filter((script) => !after.includes(script));
37674
+ return this.answer({
37675
+ ok: true,
37676
+ messages: imported.messages,
37677
+ payload: {
37678
+ uidsCreated: given,
37679
+ stillWithoutUid: after,
37680
+ note: after.length > 0 ? `${after.length} still have no .uid, which is the engine declining to import them rather than this op skipping them. No scene was written.` : "No scene was written: this reads the project and imports it, and does not resave anything."
37681
+ }
37682
+ });
37683
+ }
37565
37684
  async operation(operation, params, projectPath) {
37566
37685
  const engine = await this.engine();
37567
37686
  if (!engine.ok) {
@@ -37692,7 +37811,7 @@ class GodotServer {
37692
37811
  log.append("stderr", failed.stderr);
37693
37812
  exitCode = typeof failed.code === "number" ? failed.code : -1;
37694
37813
  } finally {
37695
- rmSync9(exportLogs, { recursive: true, force: true });
37814
+ discard(exportLogs);
37696
37815
  }
37697
37816
  log.finish();
37698
37817
  const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
@@ -37790,8 +37909,7 @@ class GodotServer {
37790
37909
  } catch (error) {
37791
37910
  reportProblem = errorMessage(error);
37792
37911
  } finally {
37793
- rmSync9(reportsDir, { recursive: true, force: true });
37794
- rmSync9(userData, { recursive: true, force: true });
37912
+ discard(reportsDir, userData);
37795
37913
  }
37796
37914
  const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
37797
37915
  const verdicts = {
@@ -38870,9 +38988,7 @@ class GodotServer {
38870
38988
  ]
38871
38989
  };
38872
38990
  } finally {
38873
- if (screenshotDir) {
38874
- rmSync9(screenshotDir, { recursive: true, force: true });
38875
- }
38991
+ discard(screenshotDir);
38876
38992
  }
38877
38993
  }
38878
38994
  async handleFindRuntimeNodes(args) {
@@ -38959,6 +39075,7 @@ var init_server3 = __esm(() => {
38959
39075
  init_resources();
38960
39076
  init_run_record();
38961
39077
  init_runtime_client();
39078
+ init_scratch();
38962
39079
  init_server_version();
38963
39080
  init_setup();
38964
39081
  init_tool_definitions();
@@ -39911,8 +40028,9 @@ function taken(harness, path, held, stripped) {
39911
40028
 
39912
40029
  // src/headless.ts
39913
40030
  init_game_log();
40031
+ init_scratch();
39914
40032
  import { execFile as execFile2 } from "node:child_process";
39915
- import { mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
40033
+ import { mkdtempSync, writeFileSync as writeFileSync2 } from "node:fs";
39916
40034
  import { tmpdir } from "node:os";
39917
40035
  import { join as join4 } from "node:path";
39918
40036
  import { promisify as promisify2 } from "node:util";
@@ -39989,7 +40107,7 @@ async function runOperation(engine, operation, params, projectPath) {
39989
40107
  messages: []
39990
40108
  };
39991
40109
  } finally {
39992
- rmSync2(paramsDir, { recursive: true, force: true });
40110
+ discard(paramsDir);
39993
40111
  }
39994
40112
  const payload = lastJsonObject(stdout);
39995
40113
  if (payload === null) {
@@ -40138,7 +40256,7 @@ init_headless();
40138
40256
  init_resources();
40139
40257
  init_server_version();
40140
40258
  import { spawnSync } from "node:child_process";
40141
- import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "node:fs";
40259
+ import { cpSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, rmSync as rmSync3, writeFileSync as writeFileSync5 } from "node:fs";
40142
40260
  import { dirname as dirname3, join as join8 } from "node:path";
40143
40261
  import { fileURLToPath } from "node:url";
40144
40262
  var ADDONS = ["gdharness_editor", "gdharness_runtime", "auto_reload"];
@@ -40163,7 +40281,7 @@ function installAddons(projectPath, from = shippedAddonsDirectory()) {
40163
40281
  }
40164
40282
  const target = join8(projectPath, "addons", name);
40165
40283
  const replaced = existsSync7(target);
40166
- rmSync4(target, { recursive: true, force: true });
40284
+ rmSync3(target, { recursive: true, force: true });
40167
40285
  mkdirSync3(dirname3(target), { recursive: true });
40168
40286
  cpSync(source, target, { recursive: true });
40169
40287
  writeFileSync5(join8(target, VERSION_MARKER), `${SERVER_VERSION}
@@ -40177,7 +40295,7 @@ function removeAddons(projectPath) {
40177
40295
  for (const name of ADDONS) {
40178
40296
  const target = join8(projectPath, "addons", name);
40179
40297
  if (existsSync7(target)) {
40180
- rmSync4(target, { recursive: true, force: true });
40298
+ rmSync3(target, { recursive: true, force: true });
40181
40299
  removed.push(target);
40182
40300
  }
40183
40301
  }
@@ -40306,7 +40424,7 @@ function inspectProject(projectPath) {
40306
40424
 
40307
40425
  // src/skill.ts
40308
40426
  init_tool_definitions();
40309
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync3, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
40427
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync3, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
40310
40428
  import { basename, dirname as dirname4, join as join9 } from "node:path";
40311
40429
 
40312
40430
  // src/tool-reference.ts
@@ -40322,7 +40440,13 @@ function namedType(declared) {
40322
40440
  return "any";
40323
40441
  }
40324
40442
  function headlessSection() {
40325
- const byTool = Object.entries(HEADLESS_OPERATIONS).map(([tool, ops]) => `\`${tool}\` (${Object.keys(ops).join(", ")})`).sort();
40443
+ const everyOp = new Map;
40444
+ for (const table of [HEADLESS_OPERATIONS, ENGINE_PASSES]) {
40445
+ for (const [tool, ops] of Object.entries(table)) {
40446
+ everyOp.set(tool, [...everyOp.get(tool) ?? [], ...Object.keys(ops)]);
40447
+ }
40448
+ }
40449
+ const byTool = [...everyOp].map(([tool, ops]) => `\`${tool}\` (${ops.join(", ")})`).sort();
40326
40450
  return [
40327
40451
  "## What a gate can call",
40328
40452
  "",
@@ -40551,7 +40675,7 @@ function removeSkill(directories, projectPath) {
40551
40675
  const removed = [];
40552
40676
  for (const directory of directories) {
40553
40677
  if (existsSync8(directory)) {
40554
- rmSync5(directory, { recursive: true, force: true });
40678
+ rmSync4(directory, { recursive: true, force: true });
40555
40679
  pruneEmpty(dirname4(directory), projectPath);
40556
40680
  removed.push(directory);
40557
40681
  }
@@ -40565,7 +40689,7 @@ function pruneEmpty(directory, projectPath) {
40565
40689
  if (readdirSync3(path).length > 0) {
40566
40690
  return;
40567
40691
  }
40568
- rmSync5(path, { recursive: true, force: true });
40692
+ rmSync4(path, { recursive: true, force: true });
40569
40693
  path = dirname4(path);
40570
40694
  }
40571
40695
  }
@@ -40573,7 +40697,7 @@ function writeSkill(directories, version) {
40573
40697
  const files = skillFiles(version);
40574
40698
  return directories.map((directory) => {
40575
40699
  const replaced = existsSync8(directory);
40576
- rmSync5(directory, { recursive: true, force: true });
40700
+ rmSync4(directory, { recursive: true, force: true });
40577
40701
  for (const [name, contents] of files) {
40578
40702
  const path = join9(directory, name);
40579
40703
  mkdirSync4(dirname4(path), { recursive: true });
@@ -108,8 +108,6 @@ func _run(operation: String, params: Dictionary) -> Dictionary:
108
108
  # Resource files
109
109
  "get_uid":
110
110
  payload = ResourceFiles.new(_log).get_uid(params)
111
- "resave_resources":
112
- payload = ResourceFiles.new(_log).resave_resources(params)
113
111
  "refresh_class_cache":
114
112
  payload = ClassCache.new(_log).refresh_class_cache(params)
115
113
 
@@ -1,10 +1,8 @@
1
1
  extends RefCounted
2
2
 
3
- const FileWalk = preload("file_walk.gd")
4
3
  const Log = preload("logger.gd")
5
4
 
6
5
  var _log: Log
7
- var _files: FileWalk = FileWalk.new()
8
6
 
9
7
 
10
8
  func _init(p_log: Log) -> void:
@@ -41,92 +39,3 @@ func get_uid(params: Dictionary) -> Dictionary:
41
39
  return {
42
40
  "file": file_path, "absolute_path": absolute_path, "uid": uid_content.strip_edges(), "exists": true
43
41
  }
44
-
45
-
46
- # Resave every scene, script and shader so that UID references are current.
47
- func resave_resources(_params: Dictionary) -> Dictionary:
48
- _log.info("Resaving all resources to update UID references...")
49
- var project_path: String = "res://"
50
-
51
- var scenes: Array[String] = _files.find_files(project_path, ".tscn")
52
- _log.debug("Found " + str(scenes.size()) + " scenes")
53
-
54
- # Resave each scene
55
- var success_count: int = 0
56
- var error_count: int = 0
57
-
58
- for scene_path: String in scenes:
59
- _log.debug("Processing scene: " + scene_path)
60
-
61
- if not FileAccess.file_exists(scene_path):
62
- _log.error("Scene file does not exist at: " + scene_path)
63
- error_count += 1
64
- continue
65
-
66
- var scene: Resource = load(scene_path)
67
- if not scene:
68
- error_count += 1
69
- _log.error("Failed to load: " + scene_path)
70
- continue
71
-
72
- var error: Error = ResourceSaver.save(scene, scene_path)
73
- if error != OK:
74
- error_count += 1
75
- _log.error("Failed to save: " + scene_path + ", error: " + str(error))
76
- continue
77
-
78
- success_count += 1
79
- if _log.debug_mode:
80
- _log.debug("Scene saved successfully: " + scene_path)
81
- var file_check_after: bool = FileAccess.file_exists(scene_path)
82
- _log.debug("File exists check after save: " + str(file_check_after))
83
- if not file_check_after:
84
- _log.error("File reported as saved but does not exist at: " + scene_path)
85
-
86
- # A UID sits beside every script and shader as a .uid file. Outside the editor the save
87
- # below answers OK and writes no sidecar, so what comes back is what was resaved and how
88
- # many are still without one, rather than a count of UIDs this made.
89
- var scripts: Array[String] = (
90
- _files.find_files(project_path, ".gd")
91
- + _files.find_files(project_path, ".shader")
92
- + _files.find_files(project_path, ".gdshader")
93
- )
94
- _log.debug("Found " + str(scripts.size()) + " scripts/shaders")
95
-
96
- var missing_uids: int = 0
97
- var resaved_scripts: int = 0
98
-
99
- for script_path: String in scripts:
100
- _log.debug("Checking UID for: " + script_path)
101
- var uid_path: String = script_path + ".uid"
102
-
103
- var f: FileAccess = FileAccess.open(uid_path, FileAccess.READ)
104
- if f:
105
- _log.debug("UID file already exists for: " + script_path)
106
- continue
107
-
108
- missing_uids += 1
109
- _log.debug("Missing UID file for: " + script_path + ", resaving...")
110
-
111
- var res: Resource = load(script_path)
112
- if not res:
113
- _log.error("Failed to load resource: " + script_path)
114
- continue
115
-
116
- var error: Error = ResourceSaver.save(res, script_path)
117
- if error != OK:
118
- _log.error("Failed to resave: " + script_path + ", error: " + str(error))
119
- continue
120
-
121
- resaved_scripts += 1
122
- _log.debug("Resaved: " + script_path)
123
-
124
- _log.info("Resave operation complete")
125
-
126
- return {
127
- "scenes_processed": scenes.size(),
128
- "scenes_saved": success_count,
129
- "scenes_with_errors": error_count,
130
- "scripts_missing_uids": missing_uids,
131
- "scripts_resaved": resaved_scripts
132
- }
package/build/index.js CHANGED
@@ -10151,8 +10151,7 @@ import {
10151
10151
  openSync as openSync2,
10152
10152
  readdirSync as readdirSync6,
10153
10153
  readFileSync as readFileSync11,
10154
- readSync,
10155
- rmSync as rmSync5
10154
+ readSync
10156
10155
  } from "node:fs";
10157
10156
  import { tmpdir as tmpdir4 } from "node:os";
10158
10157
  import { basename as basename2, dirname as dirname4, join as join11, normalize as normalize2 } from "node:path";
@@ -26324,10 +26323,35 @@ class GodotLocator {
26324
26323
 
26325
26324
  // src/headless.ts
26326
26325
  import { execFile as execFile2 } from "node:child_process";
26327
- import { mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
26326
+ import { mkdtempSync, writeFileSync as writeFileSync3 } from "node:fs";
26328
26327
  import { tmpdir as tmpdir2 } from "node:os";
26329
26328
  import { join as join6 } from "node:path";
26330
26329
  import { promisify as promisify2 } from "node:util";
26330
+
26331
+ // src/scratch.ts
26332
+ import { rmSync as rmSync2 } from "node:fs";
26333
+ var SWEPT = { recursive: true, force: true, maxRetries: 20, retryDelay: 250 };
26334
+ function discardWith(remove, paths) {
26335
+ const left = [];
26336
+ for (const path of paths) {
26337
+ if (!path) {
26338
+ continue;
26339
+ }
26340
+ try {
26341
+ remove(path);
26342
+ } catch (error) {
26343
+ left.push({ path, reason: error instanceof Error ? error.message : String(error) });
26344
+ }
26345
+ }
26346
+ return left;
26347
+ }
26348
+ function discard(...paths) {
26349
+ return discardWith((path) => {
26350
+ rmSync2(path, SWEPT);
26351
+ }, paths);
26352
+ }
26353
+
26354
+ // src/headless.ts
26331
26355
  var run2 = promisify2(execFile2);
26332
26356
  function snakeCased(params) {
26333
26357
  const result = emptyRecord();
@@ -26366,6 +26390,36 @@ function reason(stdout, stderr) {
26366
26390
  }
26367
26391
  return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
26368
26392
  }
26393
+ async function runImport(godotPath, projectPath) {
26394
+ const logDir = mkdtempSync(join6(tmpdir2(), "gdharness-import-"));
26395
+ try {
26396
+ const { stderr } = await run2(godotPath, [
26397
+ "--headless",
26398
+ "--log-file",
26399
+ join6(logDir, "engine.log"),
26400
+ "--path",
26401
+ projectPath,
26402
+ "--import"
26403
+ ]);
26404
+ return { ok: true, messages: problems(stderr) };
26405
+ } catch (error) {
26406
+ if (error instanceof Error && "stdout" in error && "stderr" in error) {
26407
+ const failed = error;
26408
+ return {
26409
+ ok: false,
26410
+ message: `the import pass failed (exit ${failed.code ?? "unknown"}): ${reason(failed.stdout, failed.stderr)}`,
26411
+ messages: problems(failed.stderr)
26412
+ };
26413
+ }
26414
+ return {
26415
+ ok: false,
26416
+ message: `the import pass could not be run: ${error instanceof Error ? error.message : String(error)}`,
26417
+ messages: []
26418
+ };
26419
+ } finally {
26420
+ discard(logDir);
26421
+ }
26422
+ }
26369
26423
  async function runOperation(engine, operation, params, projectPath) {
26370
26424
  const paramsDir = mkdtempSync(join6(tmpdir2(), "gdharness-params-"));
26371
26425
  const paramsFile = join6(paramsDir, `${operation}.json`);
@@ -26401,7 +26455,7 @@ async function runOperation(engine, operation, params, projectPath) {
26401
26455
  messages: []
26402
26456
  };
26403
26457
  } finally {
26404
- rmSync2(paramsDir, { recursive: true, force: true });
26458
+ discard(paramsDir);
26405
26459
  }
26406
26460
  const payload = lastJsonObject(stdout);
26407
26461
  if (payload === null) {
@@ -26435,7 +26489,6 @@ var HEADLESS_OPERATIONS = dictionary({
26435
26489
  set_options: "set_import_options",
26436
26490
  reimport: "reimport_resource",
26437
26491
  uid: "get_uid",
26438
- refresh_uids: "resave_resources",
26439
26492
  refresh_classes: "refresh_class_cache"
26440
26493
  }),
26441
26494
  project_export: dictionary({ list: "list_export_presets" }),
@@ -26447,6 +26500,9 @@ var HEADLESS_OPERATIONS = dictionary({
26447
26500
  inheritance: "inspect_inheritance"
26448
26501
  })
26449
26502
  });
26503
+ var ENGINE_PASSES = dictionary({
26504
+ project_import: dictionary({ refresh_uids: "import" })
26505
+ });
26450
26506
  var EDITOR_READS = dictionary({
26451
26507
  project_settings: dictionary({ get: "get_project_setting" })
26452
26508
  });
@@ -27384,6 +27440,34 @@ function projectStructure(projectPath) {
27384
27440
  visit(projectPath);
27385
27441
  return structure;
27386
27442
  }
27443
+ var CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
27444
+ function scriptsWithoutUid(projectPath) {
27445
+ const found = [];
27446
+ const visit = (directory, prefix) => {
27447
+ if (steppedOver(directory)) {
27448
+ return;
27449
+ }
27450
+ for (const entry of readdirSync3(directory, { withFileTypes: true })) {
27451
+ if (skipped(entry.name)) {
27452
+ continue;
27453
+ }
27454
+ const spelled = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
27455
+ if (entry.isDirectory()) {
27456
+ visit(join7(directory, entry.name), spelled);
27457
+ continue;
27458
+ }
27459
+ if (!entry.isFile()) {
27460
+ continue;
27461
+ }
27462
+ const extension = entry.name.split(".").pop()?.toLowerCase() ?? "";
27463
+ if (CARRIES_A_UID.has(extension) && !existsSync6(join7(directory, `${entry.name}.uid`))) {
27464
+ found.push(spelled);
27465
+ }
27466
+ }
27467
+ };
27468
+ visit(projectPath, "");
27469
+ return found.sort();
27470
+ }
27387
27471
  function searchProject(projectPath, options) {
27388
27472
  const extensions = new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
27389
27473
  const result = {
@@ -28538,7 +28622,10 @@ var TOOL_SPECS = [
28538
28622
  requires: []
28539
28623
  },
28540
28624
  uid: { summary: "the UID of one file", requires: ["resourcePath"] },
28541
- refresh_uids: { summary: "resave every resource so UID references are current", requires: [] },
28625
+ refresh_uids: {
28626
+ summary: "import the project so every script and shader has its .uid sidecar, and name under uidsCreated the ones this made and under stillWithoutUid the ones the engine would not import: it writes no scene and no script, so a project whose sidecars are all present is left untouched",
28627
+ requires: []
28628
+ },
28542
28629
  refresh_classes: {
28543
28630
  summary: 'rewrite .godot/global_script_class_cache.cfg from the class_name declarations on disk, and name under unseenByEditor any class the editor open on this project still cannot resolve: rewriting the file does not reach the list a running editor loaded, so "added: []" means the file was already right rather than that nothing is wrong',
28544
28631
  requires: []
@@ -29946,6 +30033,9 @@ class GodotServer {
29946
30033
  return { ok: true, op };
29947
30034
  }
29948
30035
  async dispatch(tool, op, args) {
30036
+ if (ENGINE_PASSES[tool]?.[op] !== undefined) {
30037
+ return await this.handleRefreshUids(args);
30038
+ }
29949
30039
  const headless = HEADLESS_OPERATIONS[tool]?.[op];
29950
30040
  if (headless !== undefined) {
29951
30041
  const { from: asked, ...answerable } = args;
@@ -30316,6 +30406,33 @@ class GodotServer {
30316
30406
  const { op: _op, projectPath: _projectPath, ...params } = contained.value;
30317
30407
  return this.answer(await this.operation(operation, params, project.value.path));
30318
30408
  }
30409
+ async handleRefreshUids(args) {
30410
+ const project = this.project(args);
30411
+ if (!project.ok) {
30412
+ return project.response;
30413
+ }
30414
+ const engine = await this.engine();
30415
+ if (!engine.ok) {
30416
+ return this.createErrorResponse("No Godot executable found.");
30417
+ }
30418
+ const projectPath = project.value.path;
30419
+ const before = scriptsWithoutUid(projectPath);
30420
+ const imported = await runImport(engine.value, projectPath);
30421
+ if (!imported.ok) {
30422
+ return this.answer(imported);
30423
+ }
30424
+ const after = scriptsWithoutUid(projectPath);
30425
+ const given = before.filter((script) => !after.includes(script));
30426
+ return this.answer({
30427
+ ok: true,
30428
+ messages: imported.messages,
30429
+ payload: {
30430
+ uidsCreated: given,
30431
+ stillWithoutUid: after,
30432
+ note: after.length > 0 ? `${after.length} still have no .uid, which is the engine declining to import them rather than this op skipping them. No scene was written.` : "No scene was written: this reads the project and imports it, and does not resave anything."
30433
+ }
30434
+ });
30435
+ }
30319
30436
  async operation(operation, params, projectPath) {
30320
30437
  const engine = await this.engine();
30321
30438
  if (!engine.ok) {
@@ -30446,7 +30563,7 @@ class GodotServer {
30446
30563
  log.append("stderr", failed.stderr);
30447
30564
  exitCode = typeof failed.code === "number" ? failed.code : -1;
30448
30565
  } finally {
30449
- rmSync5(exportLogs, { recursive: true, force: true });
30566
+ discard(exportLogs);
30450
30567
  }
30451
30568
  log.finish();
30452
30569
  const problems = log.select({ severity: "warning", sinceLastCall: false, limit: 200 });
@@ -30544,8 +30661,7 @@ class GodotServer {
30544
30661
  } catch (error) {
30545
30662
  reportProblem = errorMessage(error);
30546
30663
  } finally {
30547
- rmSync5(reportsDir, { recursive: true, force: true });
30548
- rmSync5(userData, { recursive: true, force: true });
30664
+ discard(reportsDir, userData);
30549
30665
  }
30550
30666
  const engineEntries = run.log.select({ severity: "warning", sinceLastCall: false, limit: 200 }).entries.map(aboveTheRunner);
30551
30667
  const verdicts = {
@@ -31624,9 +31740,7 @@ class GodotServer {
31624
31740
  ]
31625
31741
  };
31626
31742
  } finally {
31627
- if (screenshotDir) {
31628
- rmSync5(screenshotDir, { recursive: true, force: true });
31629
- }
31743
+ discard(screenshotDir);
31630
31744
  }
31631
31745
  }
31632
31746
  async handleFindRuntimeNodes(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.13.11",
3
+ "version": "0.13.13",
4
4
  "mcpName": "io.github.Aureliolo/gdharness",
5
5
  "description": "A harness for driving a Godot 4 project from an agent: editor addons, a runtime bridge into the running game, an MCP server in front of them, and a CLI that installs and diagnoses the Godot side.",
6
6
  "type": "module",