gdharness 0.13.12 → 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
@@ -7621,6 +7621,36 @@ function reason2(stdout, stderr) {
7621
7621
  }
7622
7622
  return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
7623
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
+ }
7624
7654
  async function runOperation2(engine, operation, params, projectPath) {
7625
7655
  const paramsDir = mkdtempSync2(join7(tmpdir3(), "gdharness-params-"));
7626
7656
  const paramsFile = join7(paramsDir, `${operation}.json`);
@@ -15475,7 +15505,10 @@ var init_tool_definitions = __esm(() => {
15475
15505
  requires: []
15476
15506
  },
15477
15507
  uid: { summary: "the UID of one file", requires: ["resourcePath"] },
15478
- 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
+ },
15479
15512
  refresh_classes: {
15480
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',
15481
15514
  requires: []
@@ -16223,7 +16256,7 @@ var init_tool_definitions = __esm(() => {
16223
16256
  });
16224
16257
 
16225
16258
  // src/headless-operations.ts
16226
- var HEADLESS_OPERATIONS, EDITOR_READS;
16259
+ var HEADLESS_OPERATIONS, ENGINE_PASSES, EDITOR_READS;
16227
16260
  var init_headless_operations = __esm(() => {
16228
16261
  HEADLESS_OPERATIONS = dictionary({
16229
16262
  project_settings: dictionary({
@@ -16245,7 +16278,6 @@ var init_headless_operations = __esm(() => {
16245
16278
  set_options: "set_import_options",
16246
16279
  reimport: "reimport_resource",
16247
16280
  uid: "get_uid",
16248
- refresh_uids: "resave_resources",
16249
16281
  refresh_classes: "refresh_class_cache"
16250
16282
  }),
16251
16283
  project_export: dictionary({ list: "list_export_presets" }),
@@ -16257,6 +16289,9 @@ var init_headless_operations = __esm(() => {
16257
16289
  inheritance: "inspect_inheritance"
16258
16290
  })
16259
16291
  });
16292
+ ENGINE_PASSES = dictionary({
16293
+ project_import: dictionary({ refresh_uids: "import" })
16294
+ });
16260
16295
  EDITOR_READS = dictionary({
16261
16296
  project_settings: dictionary({ get: "get_project_setting" })
16262
16297
  });
@@ -36049,6 +36084,33 @@ function projectStructure(projectPath) {
36049
36084
  visit(projectPath);
36050
36085
  return structure;
36051
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
+ }
36052
36114
  function searchProject(projectPath, options) {
36053
36115
  const extensions = new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
36054
36116
  const result = {
@@ -36108,10 +36170,11 @@ function searchProject(projectPath, options) {
36108
36170
  visit(projectPath);
36109
36171
  return result;
36110
36172
  }
36111
- var SKIPPED, ASSET_EXTENSIONS;
36173
+ var SKIPPED, ASSET_EXTENSIONS, CARRIES_A_UID;
36112
36174
  var init_project_scan = __esm(() => {
36113
36175
  SKIPPED = new Set(["node_modules"]);
36114
36176
  ASSET_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "svg", "ttf", "otf", "wav", "mp3", "ogg"]);
36177
+ CARRIES_A_UID = new Set(["gd", "shader", "gdshader"]);
36115
36178
  });
36116
36179
 
36117
36180
  // src/runtime-client.ts
@@ -37218,6 +37281,9 @@ class GodotServer {
37218
37281
  return { ok: true, op };
37219
37282
  }
37220
37283
  async dispatch(tool, op, args) {
37284
+ if (ENGINE_PASSES[tool]?.[op] !== undefined) {
37285
+ return await this.handleRefreshUids(args);
37286
+ }
37221
37287
  const headless = HEADLESS_OPERATIONS[tool]?.[op];
37222
37288
  if (headless !== undefined) {
37223
37289
  const { from: asked, ...answerable } = args;
@@ -37588,6 +37654,33 @@ class GodotServer {
37588
37654
  const { op: _op, projectPath: _projectPath, ...params } = contained.value;
37589
37655
  return this.answer(await this.operation(operation, params, project.value.path));
37590
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
+ }
37591
37684
  async operation(operation, params, projectPath) {
37592
37685
  const engine = await this.engine();
37593
37686
  if (!engine.ok) {
@@ -40347,7 +40440,13 @@ function namedType(declared) {
40347
40440
  return "any";
40348
40441
  }
40349
40442
  function headlessSection() {
40350
- 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();
40351
40450
  return [
40352
40451
  "## What a gate can call",
40353
40452
  "",
@@ -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
@@ -26390,6 +26390,36 @@ function reason(stdout, stderr) {
26390
26390
  }
26391
26391
  return stdout.trim().split(/\r?\n/).at(-1) ?? "no output at all";
26392
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
+ }
26393
26423
  async function runOperation(engine, operation, params, projectPath) {
26394
26424
  const paramsDir = mkdtempSync(join6(tmpdir2(), "gdharness-params-"));
26395
26425
  const paramsFile = join6(paramsDir, `${operation}.json`);
@@ -26459,7 +26489,6 @@ var HEADLESS_OPERATIONS = dictionary({
26459
26489
  set_options: "set_import_options",
26460
26490
  reimport: "reimport_resource",
26461
26491
  uid: "get_uid",
26462
- refresh_uids: "resave_resources",
26463
26492
  refresh_classes: "refresh_class_cache"
26464
26493
  }),
26465
26494
  project_export: dictionary({ list: "list_export_presets" }),
@@ -26471,6 +26500,9 @@ var HEADLESS_OPERATIONS = dictionary({
26471
26500
  inheritance: "inspect_inheritance"
26472
26501
  })
26473
26502
  });
26503
+ var ENGINE_PASSES = dictionary({
26504
+ project_import: dictionary({ refresh_uids: "import" })
26505
+ });
26474
26506
  var EDITOR_READS = dictionary({
26475
26507
  project_settings: dictionary({ get: "get_project_setting" })
26476
26508
  });
@@ -27408,6 +27440,34 @@ function projectStructure(projectPath) {
27408
27440
  visit(projectPath);
27409
27441
  return structure;
27410
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
+ }
27411
27471
  function searchProject(projectPath, options) {
27412
27472
  const extensions = new Set(options.fileTypes.map((ext) => ext.replace(/^\./, "").toLowerCase()).filter(Boolean));
27413
27473
  const result = {
@@ -28562,7 +28622,10 @@ var TOOL_SPECS = [
28562
28622
  requires: []
28563
28623
  },
28564
28624
  uid: { summary: "the UID of one file", requires: ["resourcePath"] },
28565
- 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
+ },
28566
28629
  refresh_classes: {
28567
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',
28568
28631
  requires: []
@@ -29970,6 +30033,9 @@ class GodotServer {
29970
30033
  return { ok: true, op };
29971
30034
  }
29972
30035
  async dispatch(tool, op, args) {
30036
+ if (ENGINE_PASSES[tool]?.[op] !== undefined) {
30037
+ return await this.handleRefreshUids(args);
30038
+ }
29973
30039
  const headless = HEADLESS_OPERATIONS[tool]?.[op];
29974
30040
  if (headless !== undefined) {
29975
30041
  const { from: asked, ...answerable } = args;
@@ -30340,6 +30406,33 @@ class GodotServer {
30340
30406
  const { op: _op, projectPath: _projectPath, ...params } = contained.value;
30341
30407
  return this.answer(await this.operation(operation, params, project.value.path));
30342
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
+ }
30343
30436
  async operation(operation, params, projectPath) {
30344
30437
  const engine = await this.engine();
30345
30438
  if (!engine.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdharness",
3
- "version": "0.13.12",
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",