@skillcap/gdh 3.0.2 → 3.1.0

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.
Files changed (46) hide show
  1. package/INSTALL-BUNDLE.json +1 -1
  2. package/README.md +1 -0
  3. package/RELEASE-SPAN-UPDATE-CONTRACTS.json +83 -0
  4. package/node_modules/@gdh/adapters/package.json +8 -8
  5. package/node_modules/@gdh/authoring/package.json +2 -2
  6. package/node_modules/@gdh/cli/dist/index.d.ts.map +1 -1
  7. package/node_modules/@gdh/cli/dist/index.js +195 -4
  8. package/node_modules/@gdh/cli/dist/index.js.map +1 -1
  9. package/node_modules/@gdh/cli/package.json +11 -10
  10. package/node_modules/@gdh/core/dist/bridge-substrate.d.ts +20 -0
  11. package/node_modules/@gdh/core/dist/bridge-substrate.d.ts.map +1 -0
  12. package/node_modules/@gdh/core/dist/bridge-substrate.js +40 -0
  13. package/node_modules/@gdh/core/dist/bridge-substrate.js.map +1 -0
  14. package/node_modules/@gdh/core/dist/index.d.ts +24 -29
  15. package/node_modules/@gdh/core/dist/index.d.ts.map +1 -1
  16. package/node_modules/@gdh/core/dist/index.js +12 -11
  17. package/node_modules/@gdh/core/dist/index.js.map +1 -1
  18. package/node_modules/@gdh/core/package.json +1 -1
  19. package/node_modules/@gdh/docs/package.json +2 -2
  20. package/node_modules/@gdh/editor/dist/index.d.ts +205 -0
  21. package/node_modules/@gdh/editor/dist/index.d.ts.map +1 -0
  22. package/node_modules/@gdh/editor/dist/index.js +1064 -0
  23. package/node_modules/@gdh/editor/dist/index.js.map +1 -0
  24. package/node_modules/@gdh/editor/package.json +17 -0
  25. package/node_modules/@gdh/mcp/dist/index.d.ts +1 -1
  26. package/node_modules/@gdh/mcp/dist/index.d.ts.map +1 -1
  27. package/node_modules/@gdh/mcp/dist/index.js +104 -7
  28. package/node_modules/@gdh/mcp/dist/index.js.map +1 -1
  29. package/node_modules/@gdh/mcp/package.json +10 -8
  30. package/node_modules/@gdh/observability/package.json +2 -2
  31. package/node_modules/@gdh/runtime/dist/bridge-broker-contract.d.ts +2 -2
  32. package/node_modules/@gdh/runtime/dist/bridge-broker-contract.d.ts.map +1 -1
  33. package/node_modules/@gdh/runtime/dist/bridge-broker-contract.js +2 -37
  34. package/node_modules/@gdh/runtime/dist/bridge-broker-contract.js.map +1 -1
  35. package/node_modules/@gdh/runtime/dist/bridge-surface.d.ts +1 -1
  36. package/node_modules/@gdh/runtime/dist/bridge-surface.d.ts.map +1 -1
  37. package/node_modules/@gdh/runtime/dist/bridge-surface.js +396 -73
  38. package/node_modules/@gdh/runtime/dist/bridge-surface.js.map +1 -1
  39. package/node_modules/@gdh/runtime/dist/index.d.ts +2 -3
  40. package/node_modules/@gdh/runtime/dist/index.d.ts.map +1 -1
  41. package/node_modules/@gdh/runtime/dist/index.js +2 -3
  42. package/node_modules/@gdh/runtime/dist/index.js.map +1 -1
  43. package/node_modules/@gdh/runtime/package.json +3 -2
  44. package/node_modules/@gdh/scan/package.json +3 -3
  45. package/node_modules/@gdh/verify/package.json +7 -7
  46. package/package.json +13 -11
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import {} from "@gdh/core";
4
- const RUNTIME_BRIDGE_SURFACE_VERSION = 11;
3
+ import { renderEditorOperationCatalogGdscript } from "@gdh/editor";
4
+ const RUNTIME_BRIDGE_SURFACE_VERSION = 14;
5
5
  const AUTOLOAD_ENABLE_SENTINEL = "GDH_RUNTIME_BRIDGE_AUTOLOAD_ENABLED";
6
6
  const DEFAULT_RUNTIME_BRIDGE_CONFIG = {
7
7
  enabled: true,
@@ -50,6 +50,7 @@ export async function inspectRuntimeBridgeSurface(input) {
50
50
  projectGodotPath: null,
51
51
  addonRootPath: null,
52
52
  autoloadRegistered: false,
53
+ editorPluginRegistered: false,
53
54
  customEntryPaths: [],
54
55
  managedArtifacts: [],
55
56
  };
@@ -67,6 +68,7 @@ export async function inspectRuntimeBridgeSurface(input) {
67
68
  projectGodotPath: null,
68
69
  addonRootPath: null,
69
70
  autoloadRegistered: false,
71
+ editorPluginRegistered: false,
70
72
  customEntryPaths: [],
71
73
  managedArtifacts: [],
72
74
  };
@@ -89,12 +91,14 @@ export async function inspectRuntimeBridgeSurface(input) {
89
91
  projectGodotPath,
90
92
  addonRootPath,
91
93
  autoloadRegistered: false,
94
+ editorPluginRegistered: false,
92
95
  customEntryPaths: [],
93
96
  managedArtifacts: [],
94
97
  };
95
98
  }
96
99
  const projectGodotContent = await fs.readFile(projectGodotPath, "utf8");
97
100
  const autoloadRegistered = hasAutoloadRegistration(projectGodotContent, runtimeBridge);
101
+ const editorPluginRegistered = hasEditorPluginRegistration(projectGodotContent, runtimeBridge);
98
102
  const managedArtifacts = await Promise.all(managedFiles.map((file) => inspectManagedBridgeFile({
99
103
  targetPath: resolvedTargetPath,
100
104
  targetProjectPath,
@@ -110,6 +114,7 @@ export async function inspectRuntimeBridgeSurface(input) {
110
114
  const hasPresentManagedFiles = managedArtifacts.some((artifact) => artifact.state === "present" || artifact.state === "drifted");
111
115
  const state = resolveRuntimeBridgeState({
112
116
  autoloadRegistered,
117
+ editorPluginRegistered,
113
118
  hasDrift,
114
119
  hasMissing,
115
120
  hasPresentManagedFiles,
@@ -122,6 +127,7 @@ export async function inspectRuntimeBridgeSurface(input) {
122
127
  ? ["runtime_bridge_plugin_enable_required"]
123
128
  : []),
124
129
  ...(!autoloadRegistered ? ["runtime_bridge_autoload_missing_or_drifted"] : []),
130
+ ...(!editorPluginRegistered ? ["editor_bridge_plugin_missing_or_drifted"] : []),
125
131
  ...managedArtifacts
126
132
  .filter((artifact) => artifact.state === "missing")
127
133
  .map((artifact) => `runtime_bridge_missing:${artifact.id}`),
@@ -136,6 +142,7 @@ export async function inspectRuntimeBridgeSurface(input) {
136
142
  state,
137
143
  compatibility: resolvedConfig.compatibility,
138
144
  autoloadRegistered,
145
+ editorPluginRegistered,
139
146
  runtimeBridge,
140
147
  customEntryCount: customEntryPaths.length,
141
148
  }),
@@ -147,6 +154,7 @@ export async function inspectRuntimeBridgeSurface(input) {
147
154
  projectGodotPath,
148
155
  addonRootPath,
149
156
  autoloadRegistered,
157
+ editorPluginRegistered,
150
158
  customEntryPaths,
151
159
  managedArtifacts,
152
160
  };
@@ -197,30 +205,31 @@ async function applyRuntimeBridgeOperation(input) {
197
205
  actions.push({
198
206
  id: `remove-${artifact.id}`,
199
207
  kind: "remove_file",
200
- state: artifact.state === "missing"
201
- ? "unchanged"
202
- : input.dryRun
203
- ? "planned"
204
- : "applied",
208
+ state: artifact.state === "missing" ? "unchanged" : input.dryRun ? "planned" : "applied",
205
209
  relativePath: artifact.relativePath,
206
210
  summary: artifact.state === "missing"
207
- ? "Managed runtime bridge file is already absent."
208
- : "Remove the GDH-managed runtime bridge file.",
211
+ ? "Managed bridge addon file is already absent."
212
+ : "Remove the GDH-managed bridge addon file.",
209
213
  });
210
214
  }
211
215
  actions.push({
212
216
  id: "remove-runtime-bridge-autoload",
213
217
  kind: "remove_project_godot_entry",
214
- state: !status.autoloadRegistered
215
- ? "unchanged"
216
- : input.dryRun
217
- ? "planned"
218
- : "applied",
218
+ state: !status.autoloadRegistered ? "unchanged" : input.dryRun ? "planned" : "applied",
219
219
  relativePath: path.relative(status.targetPath, projectGodotPath),
220
220
  summary: !status.autoloadRegistered
221
221
  ? "Runtime bridge autoload entry is already absent."
222
222
  : "Remove the GDH runtime bridge autoload entry left by older bridge installs.",
223
223
  });
224
+ actions.push({
225
+ id: "remove-editor-bridge-plugin-registration",
226
+ kind: "remove_project_godot_entry",
227
+ state: !status.editorPluginRegistered ? "unchanged" : input.dryRun ? "planned" : "applied",
228
+ relativePath: path.relative(status.targetPath, projectGodotPath),
229
+ summary: !status.editorPluginRegistered
230
+ ? "GDH editor plugin registration is already absent."
231
+ : "Remove the GDH editor plugin registration from project.godot.",
232
+ });
224
233
  if (!input.dryRun) {
225
234
  for (const artifact of status.managedArtifacts) {
226
235
  if (artifact.state === "missing") {
@@ -232,17 +241,17 @@ async function applyRuntimeBridgeOperation(input) {
232
241
  const content = await fs.readFile(projectGodotPath, "utf8");
233
242
  await fs.writeFile(projectGodotPath, removeAutoloadRegistration(content, status.runtimeBridge), "utf8");
234
243
  }
244
+ if (status.editorPluginRegistered) {
245
+ const content = await fs.readFile(projectGodotPath, "utf8");
246
+ await fs.writeFile(projectGodotPath, removeEditorPluginRegistration(content, status.runtimeBridge), "utf8");
247
+ }
235
248
  }
236
249
  }
237
250
  else {
238
251
  actions.push({
239
252
  id: "write-project-bridge-entries-gitkeep",
240
253
  kind: "write_file",
241
- state: projectEntryGitkeepExists
242
- ? "unchanged"
243
- : input.dryRun
244
- ? "planned"
245
- : "applied",
254
+ state: projectEntryGitkeepExists ? "unchanged" : input.dryRun ? "planned" : "applied",
246
255
  relativePath: normalizePath(path.relative(status.targetPath, projectEntryGitkeepPath)),
247
256
  summary: projectEntryGitkeepExists
248
257
  ? "Project-owned runtime bridge entry folder scaffold already exists."
@@ -253,30 +262,31 @@ async function applyRuntimeBridgeOperation(input) {
253
262
  actions.push({
254
263
  id: `write-${file.id}`,
255
264
  kind: "write_file",
256
- state: artifact.state === "present"
257
- ? "unchanged"
258
- : input.dryRun
259
- ? "planned"
260
- : "applied",
265
+ state: artifact.state === "present" ? "unchanged" : input.dryRun ? "planned" : "applied",
261
266
  relativePath: artifact.relativePath,
262
267
  summary: artifact.state === "present"
263
- ? "Managed runtime bridge file already matches the current GDH baseline."
264
- : "Write the GDH-managed runtime bridge file.",
268
+ ? "Managed bridge addon file already matches the current GDH baseline."
269
+ : "Write the GDH-managed bridge addon file.",
265
270
  });
266
271
  }
267
272
  actions.push({
268
273
  id: "write-runtime-bridge-autoload",
269
274
  kind: "update_project_godot",
270
- state: status.autoloadRegistered
271
- ? "unchanged"
272
- : input.dryRun
273
- ? "planned"
274
- : "applied",
275
+ state: status.autoloadRegistered ? "unchanged" : input.dryRun ? "planned" : "applied",
275
276
  relativePath: path.relative(status.targetPath, projectGodotPath),
276
277
  summary: status.autoloadRegistered
277
278
  ? "Runtime bridge autoload entry is already present."
278
279
  : "Register the GDH runtime bridge autoload in project.godot (class-3 register_autoload op, GDH_MANAGED_SURFACE_CLASSES allowlist).",
279
280
  });
281
+ actions.push({
282
+ id: "write-editor-bridge-plugin-registration",
283
+ kind: "update_project_godot",
284
+ state: status.editorPluginRegistered ? "unchanged" : input.dryRun ? "planned" : "applied",
285
+ relativePath: path.relative(status.targetPath, projectGodotPath),
286
+ summary: status.editorPluginRegistered
287
+ ? "GDH editor plugin registration is already present."
288
+ : "Register the GDH editor plugin in project.godot (class-3 edit_editor_plugins_array op, GDH_MANAGED_SURFACE_CLASSES allowlist).",
289
+ });
280
290
  if (!input.dryRun) {
281
291
  if (!projectEntryGitkeepExists) {
282
292
  await fs.mkdir(path.dirname(projectEntryGitkeepPath), { recursive: true });
@@ -295,6 +305,10 @@ async function applyRuntimeBridgeOperation(input) {
295
305
  const content = await fs.readFile(projectGodotPath, "utf8");
296
306
  await fs.writeFile(projectGodotPath, addAutoloadRegistration(content, status.runtimeBridge), "utf8");
297
307
  }
308
+ if (!status.editorPluginRegistered) {
309
+ const content = await fs.readFile(projectGodotPath, "utf8");
310
+ await fs.writeFile(projectGodotPath, addEditorPluginRegistration(content, status.runtimeBridge), "utf8");
311
+ }
298
312
  }
299
313
  }
300
314
  const nextStatus = input.dryRun
@@ -328,7 +342,7 @@ async function inspectManagedBridgeFile(input) {
328
342
  absolutePath,
329
343
  managed: true,
330
344
  state: "missing",
331
- summary: "Managed runtime bridge file has not been installed yet.",
345
+ summary: "Managed bridge addon file has not been installed yet.",
332
346
  expectedVersion: input.file.expectedVersion,
333
347
  detectedVersion: null,
334
348
  };
@@ -340,8 +354,8 @@ async function inspectManagedBridgeFile(input) {
340
354
  managed: true,
341
355
  state: currentContent === input.file.content ? "present" : "drifted",
342
356
  summary: currentContent === input.file.content
343
- ? "Managed runtime bridge file matches the current GDH baseline."
344
- : "Managed runtime bridge file exists but has drifted from the current GDH baseline.",
357
+ ? "Managed bridge addon file matches the current GDH baseline."
358
+ : "Managed bridge addon file exists but has drifted from the current GDH baseline.",
345
359
  expectedVersion: input.file.expectedVersion,
346
360
  detectedVersion: input.file.relativePath.endsWith(".json")
347
361
  ? parseBridgeManifestVersion(currentContent)
@@ -378,6 +392,7 @@ function renderManagedBridgeFiles(runtimeBridge) {
378
392
  managedFiles: [
379
393
  "plugin.cfg",
380
394
  "plugin.gd",
395
+ "editor/gdh_editor_bridge_plugin.gd",
381
396
  "registry/bridge_registry.gd",
382
397
  "entries/core_entries.gd",
383
398
  "runtime/gdh_runtime_bridge.gd",
@@ -390,11 +405,11 @@ function renderManagedBridgeFiles(runtimeBridge) {
390
405
  relativePath: normalizePath(path.join(runtimeBridge.addonPath, "plugin.cfg")),
391
406
  expectedVersion: RUNTIME_BRIDGE_SURFACE_VERSION,
392
407
  content: [
393
- `; GDH managed runtime bridge v${RUNTIME_BRIDGE_SURFACE_VERSION}`,
408
+ `; GDH managed bridge addon v${RUNTIME_BRIDGE_SURFACE_VERSION}`,
394
409
  "",
395
410
  "[plugin]",
396
- 'name="GDH Runtime Bridge"',
397
- 'description="GDH-managed bounded runtime bridge surface."',
411
+ 'name="GDH Bridge"',
412
+ 'description="GDH-managed editor and runtime bridge surfaces."',
398
413
  'author="GDH"',
399
414
  `version="${RUNTIME_BRIDGE_SURFACE_VERSION}"`,
400
415
  'script="plugin.gd"',
@@ -409,12 +424,31 @@ function renderManagedBridgeFiles(runtimeBridge) {
409
424
  "@tool",
410
425
  "extends EditorPlugin",
411
426
  "",
412
- `const GDH_RUNTIME_BRIDGE_VERSION := ${RUNTIME_BRIDGE_SURFACE_VERSION}`,
427
+ `const GDH_BRIDGE_ADDON_VERSION := ${RUNTIME_BRIDGE_SURFACE_VERSION}`,
428
+ `const EditorBridgePlugin = preload("res://${normalizePath(path.join(runtimeBridge.addonPath, "editor", "gdh_editor_bridge_plugin.gd"))}")`,
413
429
  `const AUTOLOAD_NAME := "${runtimeBridge.autoloadName}"`,
414
430
  `const AUTOLOAD_PATH := "res://${normalizePath(path.join(runtimeBridge.addonPath, "runtime", "gdh_runtime_bridge.gd"))}"`,
415
431
  "",
432
+ "var _editor_bridge: RefCounted",
433
+ "",
434
+ "func _enter_tree() -> void:",
435
+ "\tset_process(true)",
436
+ "\t_editor_bridge = EditorBridgePlugin.new()",
437
+ '\tif _editor_bridge != null and _editor_bridge.has_method("enter_tree"):',
438
+ "\t\t_editor_bridge.enter_tree(self)",
439
+ "",
440
+ "func _process(delta: float) -> void:",
441
+ '\tif _editor_bridge != null and _editor_bridge.has_method("process"):',
442
+ "\t\t_editor_bridge.process(delta)",
443
+ "",
444
+ "func _exit_tree() -> void:",
445
+ "\tset_process(false)",
446
+ '\tif _editor_bridge != null and _editor_bridge.has_method("exit_tree"):',
447
+ "\t\t_editor_bridge.exit_tree(self)",
448
+ "\t_editor_bridge = null",
449
+ "",
416
450
  "func _enable_plugin() -> void:",
417
- "\tif not ProjectSettings.has_setting(\"autoload/%s\" % AUTOLOAD_NAME):",
451
+ '\tif not ProjectSettings.has_setting("autoload/%s" % AUTOLOAD_NAME):',
418
452
  "\t\tadd_autoload_singleton(AUTOLOAD_NAME, AUTOLOAD_PATH)",
419
453
  `\t\tProjectSettings.set_setting("${AUTOLOAD_ENABLE_SENTINEL}", true)`,
420
454
  "",
@@ -425,6 +459,231 @@ function renderManagedBridgeFiles(runtimeBridge) {
425
459
  "",
426
460
  ].join("\n"),
427
461
  },
462
+ {
463
+ id: "editor-bridge-plugin-gd",
464
+ relativePath: normalizePath(path.join(runtimeBridge.addonPath, "editor", "gdh_editor_bridge_plugin.gd")),
465
+ expectedVersion: RUNTIME_BRIDGE_SURFACE_VERSION,
466
+ content: [
467
+ "@tool",
468
+ "extends RefCounted",
469
+ "",
470
+ `const GDH_EDITOR_BRIDGE_VERSION := ${RUNTIME_BRIDGE_SURFACE_VERSION}`,
471
+ "const GDH_EDITOR_ADOPTION_PROTOCOL_VERSION := 1",
472
+ 'const GDH_EDITOR_ADOPTION_HOST := "127.0.0.1"',
473
+ "const GDH_EDITOR_ADOPTION_PORT_START := 42240",
474
+ "const GDH_EDITOR_ADOPTION_PORT_END := 42320",
475
+ "const GDH_EDITOR_HEARTBEAT_INTERVAL_SECONDS := 2.0",
476
+ "",
477
+ "var _plugin: EditorPlugin",
478
+ "var _server := TCPServer.new()",
479
+ "var _peers: Array = []",
480
+ "var _heartbeat_elapsed := 0.0",
481
+ 'var _target_root_path := ""',
482
+ 'var _godot_project_root_path := ""',
483
+ 'var _state_root_path := ""',
484
+ 'var _editor_bridge_dir := ""',
485
+ 'var _metadata_path := ""',
486
+ 'var _token_file_path := ""',
487
+ 'var _heartbeat_path := ""',
488
+ 'var _token := ""',
489
+ "var _port := 0",
490
+ 'var _started_at := ""',
491
+ "",
492
+ "func enter_tree(plugin: EditorPlugin) -> void:",
493
+ "\t_plugin = plugin",
494
+ "\t_started_at = _now_iso()",
495
+ '\t_godot_project_root_path = _normalize_absolute_path(ProjectSettings.globalize_path("res://"))',
496
+ "\t_target_root_path = _find_target_root(_godot_project_root_path)",
497
+ '\t_state_root_path = _normalize_absolute_path(_target_root_path.path_join(".gdh-state"))',
498
+ '\t_editor_bridge_dir = _state_root_path.path_join("editor-bridge")',
499
+ '\t_metadata_path = _editor_bridge_dir.path_join("adopted-editor.json")',
500
+ '\t_token_file_path = _editor_bridge_dir.path_join("adopted-editor.token")',
501
+ '\t_heartbeat_path = _editor_bridge_dir.path_join("adopted-editor.heartbeat")',
502
+ "\tDirAccess.make_dir_recursive_absolute(_editor_bridge_dir)",
503
+ "\t_token = _read_or_create_token(_token_file_path)",
504
+ "\t_start_server()",
505
+ "\t_write_heartbeat()",
506
+ "",
507
+ "func process(delta: float) -> void:",
508
+ "\t_poll_server()",
509
+ "\t_heartbeat_elapsed += delta",
510
+ "\tif _heartbeat_elapsed >= GDH_EDITOR_HEARTBEAT_INTERVAL_SECONDS:",
511
+ "\t\t_heartbeat_elapsed = 0.0",
512
+ "\t\t_write_heartbeat()",
513
+ "",
514
+ "func exit_tree(_plugin_instance: EditorPlugin) -> void:",
515
+ "\t_server.stop()",
516
+ "\t_peers.clear()",
517
+ "\tDirAccess.remove_absolute(_metadata_path)",
518
+ "\tDirAccess.remove_absolute(_heartbeat_path)",
519
+ "\t_plugin = null",
520
+ "",
521
+ "func get_status() -> Dictionary:",
522
+ "\treturn {",
523
+ '\t\t"version": GDH_EDITOR_BRIDGE_VERSION,',
524
+ '\t\t"state": "ready",',
525
+ '\t\t"capabilities": [',
526
+ '\t\t\t"editor_operation_catalog",',
527
+ '\t\t\t"editor_session_adoption_anchor",',
528
+ "\t\t],",
529
+ "\t}",
530
+ "",
531
+ "func _start_server() -> void:",
532
+ "\tfor candidate_port in range(GDH_EDITOR_ADOPTION_PORT_START, GDH_EDITOR_ADOPTION_PORT_END + 1):",
533
+ "\t\tif _server.listen(candidate_port, GDH_EDITOR_ADOPTION_HOST) == OK:",
534
+ "\t\t\t_port = candidate_port",
535
+ "\t\t\treturn",
536
+ "\t_server.stop()",
537
+ "\t_port = 0",
538
+ "",
539
+ "func _poll_server() -> void:",
540
+ "\tif _port <= 0:",
541
+ "\t\treturn",
542
+ "\twhile _server.is_connection_available():",
543
+ "\t\tvar peer := _server.take_connection()",
544
+ '\t\t_peers.append({"peer": peer, "buffer": PackedByteArray()})',
545
+ "\tvar remaining: Array = []",
546
+ "\tfor entry in _peers:",
547
+ '\t\tvar peer: StreamPeerTCP = entry["peer"]',
548
+ '\t\tvar buffer: PackedByteArray = entry["buffer"]',
549
+ "\t\tif peer.get_status() == StreamPeerTCP.STATUS_CONNECTED:",
550
+ "\t\t\tvar available := peer.get_available_bytes()",
551
+ "\t\t\tif available > 0:",
552
+ "\t\t\t\tvar chunk := peer.get_data(available)",
553
+ "\t\t\t\tif int(chunk[0]) == OK:",
554
+ "\t\t\t\t\tbuffer.append_array(chunk[1])",
555
+ "\t\t\tif _try_handle_peer(peer, buffer):",
556
+ "\t\t\t\tpeer.disconnect_from_host()",
557
+ "\t\t\telse:",
558
+ "\t\t\t\tremaining.append(entry)",
559
+ "\t_peers = remaining",
560
+ "",
561
+ "func _try_handle_peer(peer: StreamPeerTCP, buffer: PackedByteArray) -> bool:",
562
+ "\tvar text := buffer.get_string_from_utf8()",
563
+ '\tvar header_end := text.find("\\r\\n\\r\\n")',
564
+ "\tif header_end < 0:",
565
+ "\t\treturn false",
566
+ "\tvar header_text := text.substr(0, header_end)",
567
+ "\tvar body_start := header_end + 4",
568
+ "\tvar content_length := 0",
569
+ "\tvar authorized := false",
570
+ '\tfor raw_header in header_text.split("\\r\\n"):',
571
+ "\t\tvar header := str(raw_header)",
572
+ "\t\tvar lower := header.to_lower()",
573
+ '\t\tif lower.begins_with("content-length:"):',
574
+ '\t\t\tcontent_length = int(header.substr(header.find(":") + 1).strip_edges())',
575
+ '\t\telif lower.begins_with("authorization:"):',
576
+ '\t\t\tauthorized = header.substr(header.find(":") + 1).strip_edges() == "Bearer " + _token',
577
+ "\tif text.length() < body_start + content_length:",
578
+ "\t\treturn false",
579
+ "\tif not authorized:",
580
+ '\t\t_write_http_response(peer, 401, _failure("Adopted editor token is invalid.", ["adopted_editor_token_invalid"]))',
581
+ "\t\treturn true",
582
+ "\tvar request_text := text.substr(body_start, content_length)",
583
+ "\tvar request = JSON.parse_string(request_text)",
584
+ "\tif typeof(request) != TYPE_DICTIONARY:",
585
+ '\t\t_write_http_response(peer, 400, _failure("Invalid adopted editor request JSON.", ["request_json_invalid"]))',
586
+ "\t\treturn true",
587
+ "\t_write_http_response(peer, 200, _handle_adopted_request(request))",
588
+ "\treturn true",
589
+ "",
590
+ "func _handle_adopted_request(request: Dictionary) -> Dictionary:",
591
+ '\tif int(request.get("protocolVersion", 0)) != GDH_EDITOR_ADOPTION_PROTOCOL_VERSION:',
592
+ '\t\treturn _failure("Adopted editor protocol version mismatch.", ["adopted_editor_protocol_mismatch"])',
593
+ '\tvar target_identity = request.get("targetIdentity", {})',
594
+ '\tif typeof(target_identity) != TYPE_DICTIONARY or str(target_identity.get("identityKey", "")) != str(_target_identity().get("identityKey", "")):',
595
+ '\t\treturn _failure("Adopted editor target identity mismatch.", ["adopted_editor_identity_mismatch"])',
596
+ '\tvar operation = request.get("operation", {})',
597
+ "\tif typeof(operation) != TYPE_DICTIONARY:",
598
+ '\t\treturn _failure("Missing adopted editor operation payload.", ["operation_missing"])',
599
+ "\treturn run_editor_operation(operation)",
600
+ "",
601
+ "func _write_http_response(peer: StreamPeerTCP, status_code: int, body: Dictionary) -> void:",
602
+ "\tvar body_text := JSON.stringify(body)",
603
+ '\tvar status_text := "OK" if status_code == 200 else "Error"',
604
+ '\tvar response := "HTTP/1.1 %d %s\\r\\nContent-Type: application/json\\r\\nContent-Length: %d\\r\\nConnection: close\\r\\n\\r\\n%s" % [status_code, status_text, body_text.to_utf8_buffer().size(), body_text]',
605
+ "\tpeer.put_data(response.to_utf8_buffer())",
606
+ "",
607
+ "func _write_heartbeat() -> void:",
608
+ "\tif _port <= 0:",
609
+ "\t\treturn",
610
+ "\tvar now := _now_iso()",
611
+ '\t_write_text_file(_heartbeat_path, now + "\\n")',
612
+ "\t_write_text_file(_metadata_path, JSON.stringify({",
613
+ '\t\t"protocolVersion": GDH_EDITOR_ADOPTION_PROTOCOL_VERSION,',
614
+ '\t\t"bridgeSurfaceVersion": GDH_EDITOR_BRIDGE_VERSION,',
615
+ '\t\t"targetIdentity": _target_identity(),',
616
+ '\t\t"pid": OS.get_process_id(),',
617
+ '\t\t"host": GDH_EDITOR_ADOPTION_HOST,',
618
+ '\t\t"port": _port,',
619
+ '\t\t"metadataPath": _metadata_path,',
620
+ '\t\t"tokenFilePath": _token_file_path,',
621
+ '\t\t"heartbeatPath": _heartbeat_path,',
622
+ '\t\t"startedAt": _started_at,',
623
+ '\t\t"lastHeartbeatAt": now,',
624
+ '\t}, "\\t") + "\\n")',
625
+ "",
626
+ "func _target_identity() -> Dictionary:",
627
+ '\tvar path_style := "win32" if OS.get_name() == "Windows" else "posix"',
628
+ "\tvar normalized_target := _normalize_identity_path(_target_root_path, path_style)",
629
+ "\tvar normalized_project := _normalize_identity_path(_godot_project_root_path, path_style)",
630
+ "\tvar normalized_state := _normalize_identity_path(_state_root_path, path_style)",
631
+ "\treturn {",
632
+ '\t\t"targetRootPath": _target_root_path,',
633
+ '\t\t"godotProjectRootPath": _godot_project_root_path,',
634
+ '\t\t"stateRootPath": _state_root_path,',
635
+ '\t\t"normalizedTargetRootPath": normalized_target,',
636
+ '\t\t"normalizedGodotProjectRootPath": normalized_project,',
637
+ '\t\t"normalizedStateRootPath": normalized_state,',
638
+ '\t\t"pathStyle": path_style,',
639
+ '\t\t"identityKey": "target=%s|godot=%s|state=%s|style=%s" % [normalized_target, normalized_project, normalized_state, path_style],',
640
+ "\t}",
641
+ "",
642
+ "func _read_or_create_token(token_path: String) -> String:",
643
+ "\tif FileAccess.file_exists(token_path):",
644
+ "\t\treturn FileAccess.get_file_as_string(token_path).strip_edges()",
645
+ "\tvar token := _generate_token()",
646
+ '\t_write_text_file(token_path, token + "\\n")',
647
+ "\treturn token",
648
+ "",
649
+ "func _generate_token() -> String:",
650
+ "\tvar crypto := Crypto.new()",
651
+ "\treturn crypto.generate_random_bytes(32).hex_encode()",
652
+ "",
653
+ "func _write_text_file(file_path: String, content: String) -> void:",
654
+ "\tDirAccess.make_dir_recursive_absolute(file_path.get_base_dir())",
655
+ "\tvar file := FileAccess.open(file_path, FileAccess.WRITE)",
656
+ "\tif file:",
657
+ "\t\tfile.store_string(content)",
658
+ "\t\tfile.close()",
659
+ "",
660
+ "func _find_target_root(project_root: String) -> String:",
661
+ "\tvar current := _normalize_absolute_path(project_root)",
662
+ "\tfor _index in range(0, 64):",
663
+ '\t\tif FileAccess.file_exists(current.path_join(".gdh/project.yaml")):',
664
+ "\t\t\treturn current",
665
+ "\t\tvar parent := current.get_base_dir()",
666
+ '\t\tif parent == current or parent == "":',
667
+ "\t\t\treturn _normalize_absolute_path(project_root)",
668
+ "\t\tcurrent = parent",
669
+ "\treturn _normalize_absolute_path(project_root)",
670
+ "",
671
+ "func _normalize_absolute_path(value: String) -> String:",
672
+ '\tvar normalized := value.simplify_path().trim_suffix("/").trim_suffix("\\\\")',
673
+ "\treturn normalized",
674
+ "",
675
+ "func _normalize_identity_path(value: String, path_style: String) -> String:",
676
+ "\tvar normalized := _normalize_absolute_path(value)",
677
+ '\tif path_style == "win32":',
678
+ '\t\treturn normalized.replace("/", "\\\\").to_lower()',
679
+ "\treturn normalized",
680
+ "",
681
+ "func _now_iso() -> String:",
682
+ '\treturn Time.get_datetime_string_from_system(true) + "Z"',
683
+ "",
684
+ renderEditorOperationCatalogGdscript().trimEnd(),
685
+ ].join("\n"),
686
+ },
428
687
  {
429
688
  id: "bridge-registry-gd",
430
689
  relativePath: normalizePath(path.join(runtimeBridge.addonPath, "registry", "bridge_registry.gd")),
@@ -439,18 +698,18 @@ function renderManagedBridgeFiles(runtimeBridge) {
439
698
  "static func list_entries() -> Array[Dictionary]:",
440
699
  "\tvar entries: Array[Dictionary] = CoreEntries.list_entries()",
441
700
  "\tfor project_entries in _load_project_entries():",
442
- "\t\tif project_entries != null and project_entries.has_method(\"list_entries\"):",
701
+ '\t\tif project_entries != null and project_entries.has_method("list_entries"):',
443
702
  "\t\t\tentries.append_array(project_entries.list_entries())",
444
703
  "\treturn entries",
445
704
  "",
446
705
  "static func invoke(runtime: Node, entry_id: String, input: Dictionary) -> Dictionary:",
447
706
  "\tvar core_result := CoreEntries.invoke(runtime, entry_id, input)",
448
- "\tif str(core_result.get(\"state\", \"\")) != \"unavailable\":",
707
+ '\tif str(core_result.get("state", "")) != "unavailable":',
449
708
  "\t\treturn core_result",
450
709
  "\tfor project_entries in _load_project_entries():",
451
- "\t\tif project_entries != null and project_entries.has_method(\"invoke\"):",
710
+ '\t\tif project_entries != null and project_entries.has_method("invoke"):',
452
711
  "\t\t\tvar project_result: Dictionary = project_entries.invoke(runtime, entry_id, input)",
453
- "\t\t\tif str(project_result.get(\"state\", \"\")) != \"unavailable\":",
712
+ '\t\t\tif str(project_result.get("state", "")) != "unavailable":',
454
713
  "\t\t\t\treturn project_result",
455
714
  "\treturn core_result",
456
715
  "",
@@ -477,10 +736,10 @@ function renderManagedBridgeFiles(runtimeBridge) {
477
736
  "\tdirectory.list_dir_begin()",
478
737
  "\tvar file_name := directory.get_next()",
479
738
  "\twhile not file_name.is_empty():",
480
- "\t\tvar child_path := \"%s/%s\" % [directory_path, file_name]",
739
+ '\t\tvar child_path := "%s/%s" % [directory_path, file_name]',
481
740
  "\t\tif directory.current_is_dir():",
482
741
  "\t\t\t_collect_project_entry_paths(child_path, paths)",
483
- "\t\telif file_name.ends_with(\".gd\"):",
742
+ '\t\telif file_name.ends_with(".gd"):',
484
743
  "\t\t\tpaths.append(child_path)",
485
744
  "\t\tfile_name = directory.get_next()",
486
745
  "\tdirectory.list_dir_end()",
@@ -713,7 +972,7 @@ function renderManagedBridgeFiles(runtimeBridge) {
713
972
  '\t\t"input.action.release":',
714
973
  "\t\t\treturn _set_input_action(input, false)",
715
974
  '\t\t"input.action.hold":',
716
- "\t\t\treturn _result(\"unavailable\", null, \"input.action.hold is handled by the GDH runtime dispatch path.\")",
975
+ '\t\t\treturn _result("unavailable", null, "input.action.hold is handled by the GDH runtime dispatch path.")',
717
976
  '\t\t"input.event.send":',
718
977
  "\t\t\treturn _send_input_event(input)",
719
978
  '\t\t"ui.button.press":',
@@ -774,7 +1033,7 @@ function renderManagedBridgeFiles(runtimeBridge) {
774
1033
  '\tvar node_path: String = str(input.get("nodePath", ""))',
775
1034
  '\tvar signal_name: String = str(input.get("signalName", ""))',
776
1035
  "",
777
- '\tif node_path.is_empty() or signal_name.is_empty():',
1036
+ "\tif node_path.is_empty() or signal_name.is_empty():",
778
1037
  '\t\treturn _result("failed", null, "state.signal_observation.start requires nodePath and signalName.")',
779
1038
  "",
780
1039
  "\treturn runtime.start_signal_observation(node_path, signal_name)",
@@ -783,7 +1042,7 @@ function renderManagedBridgeFiles(runtimeBridge) {
783
1042
  '\tvar node_path: String = str(input.get("nodePath", ""))',
784
1043
  '\tvar signal_name: String = str(input.get("signalName", ""))',
785
1044
  "",
786
- '\tif node_path.is_empty() or signal_name.is_empty():',
1045
+ "\tif node_path.is_empty() or signal_name.is_empty():",
787
1046
  '\t\treturn _result("failed", null, "state.signal_observation.get requires nodePath and signalName.")',
788
1047
  "",
789
1048
  "\treturn runtime.get_signal_observation_snapshot(node_path, signal_name)",
@@ -792,7 +1051,7 @@ function renderManagedBridgeFiles(runtimeBridge) {
792
1051
  '\tvar node_path: String = str(input.get("nodePath", ""))',
793
1052
  '\tvar property_name: String = str(input.get("property", ""))',
794
1053
  "",
795
- '\tif node_path.is_empty() or property_name.is_empty():',
1054
+ "\tif node_path.is_empty() or property_name.is_empty():",
796
1055
  '\t\treturn _result("failed", null, "state.node_property.get requires nodePath and property.")',
797
1056
  "",
798
1057
  "\tvar node: Node = runtime.resolve_node_by_path(node_path)",
@@ -1103,7 +1362,7 @@ function renderManagedBridgeFiles(runtimeBridge) {
1103
1362
  '\tvar requested_class_name: String = str(filters.get("className", ""))',
1104
1363
  "",
1105
1364
  "\t_append_node_if_matches(search_root, name_contains, requested_class_name, limit, results)",
1106
- "\tfor node in search_root.find_children(\"*\", requested_class_name, true, false):",
1365
+ '\tfor node in search_root.find_children("*", requested_class_name, true, false):',
1107
1366
  "\t\tif results.size() >= limit:",
1108
1367
  "\t\t\tbreak",
1109
1368
  "\t\t_append_node_if_matches(node, name_contains, requested_class_name, limit, results)",
@@ -1118,7 +1377,7 @@ function renderManagedBridgeFiles(runtimeBridge) {
1118
1377
  "",
1119
1378
  "func start_signal_observation(node_path: String, signal_name: String) -> Dictionary:",
1120
1379
  '\tvar entry_id := "state.signal_observation.start"',
1121
- '\tvar observation_key := _build_signal_observation_key(node_path, signal_name)',
1380
+ "\tvar observation_key := _build_signal_observation_key(node_path, signal_name)",
1122
1381
  "",
1123
1382
  "\tif _signal_observations.has(observation_key):",
1124
1383
  "\t\treturn _bridge_ok(_duplicate_signal_observation(_signal_observations[observation_key]))",
@@ -1150,7 +1409,7 @@ function renderManagedBridgeFiles(runtimeBridge) {
1150
1409
  "",
1151
1410
  "func get_signal_observation_snapshot(node_path: String, signal_name: String) -> Dictionary:",
1152
1411
  '\tvar entry_id := "state.signal_observation.get"',
1153
- '\tvar observation_key := _build_signal_observation_key(node_path, signal_name)',
1412
+ "\tvar observation_key := _build_signal_observation_key(node_path, signal_name)",
1154
1413
  "",
1155
1414
  "\tif not _signal_observations.has(observation_key):",
1156
1415
  '\t\treturn _bridge_result("unavailable", null, "%s has not started observing %s on node %s." % [entry_id, signal_name, node_path])',
@@ -1195,11 +1454,11 @@ function renderManagedBridgeFiles(runtimeBridge) {
1195
1454
  "\targ2: Variant = null,",
1196
1455
  "\targ3: Variant = null,",
1197
1456
  ") -> void:",
1198
- '\tvar observation_key := _build_signal_observation_key(node_path, signal_name)',
1457
+ "\tvar observation_key := _build_signal_observation_key(node_path, signal_name)",
1199
1458
  "\tif not _signal_observations.has(observation_key):",
1200
1459
  "\t\treturn",
1201
1460
  "",
1202
- '\tvar snapshot: Dictionary = _signal_observations[observation_key]',
1461
+ "\tvar snapshot: Dictionary = _signal_observations[observation_key]",
1203
1462
  '\tsnapshot["count"] = int(snapshot.get("count", 0)) + 1',
1204
1463
  '\tsnapshot["lastObservedAt"] = Time.get_datetime_string_from_system(true, true)',
1205
1464
  '\tsnapshot["lastPayload"] = _normalize_signal_payload(arg0, arg1, arg2, arg3)',
@@ -1247,9 +1506,9 @@ function renderManagedBridgeFiles(runtimeBridge) {
1247
1506
  "\tif has_duration == has_physics_frames:",
1248
1507
  '\t\treturn _bridge_result("failed", null, "input.action.hold requires exactly one of durationMs or physicsFrames.")',
1249
1508
  "",
1250
- '\tvar started_at_ms := Time.get_ticks_msec()',
1251
- '\tvar duration_ms := 0',
1252
- '\tvar physics_frames := 0',
1509
+ "\tvar started_at_ms := Time.get_ticks_msec()",
1510
+ "\tvar duration_ms := 0",
1511
+ "\tvar physics_frames := 0",
1253
1512
  '\tvar mode := "durationMs" if has_duration else "physicsFrames"',
1254
1513
  "",
1255
1514
  "\tInput.action_press(action)",
@@ -1262,7 +1521,7 @@ function renderManagedBridgeFiles(runtimeBridge) {
1262
1521
  "\t\t\tawait get_tree().physics_frame",
1263
1522
  "\tInput.action_release(action)",
1264
1523
  "",
1265
- '\tvar finished_at_ms := Time.get_ticks_msec()',
1524
+ "\tvar finished_at_ms := Time.get_ticks_msec()",
1266
1525
  "\treturn _bridge_ok(",
1267
1526
  "\t\t{",
1268
1527
  '\t\t\t"action": action,',
@@ -1377,9 +1636,9 @@ function renderManagedBridgeFiles(runtimeBridge) {
1377
1636
  "\tfile.store_string(JSON.stringify(payload))",
1378
1637
  "\tfile.close()",
1379
1638
  "",
1380
- "\tif state == \"captured\":",
1639
+ '\tif state == "captured":',
1381
1640
  "\t\treturn _bridge_ok(payload)",
1382
- '\treturn _bridge_result(state, payload, summary)',
1641
+ "\treturn _bridge_result(state, payload, summary)",
1383
1642
  "",
1384
1643
  "func _accept_pending_connection() -> void:",
1385
1644
  "\twhile _server.is_connection_available():",
@@ -1505,7 +1764,7 @@ function resolveRuntimeBridgeState(input) {
1505
1764
  if (input.hasDrift) {
1506
1765
  return "drifted";
1507
1766
  }
1508
- if (!input.autoloadRegistered) {
1767
+ if (!input.autoloadRegistered || !input.editorPluginRegistered) {
1509
1768
  return input.hasPresentManagedFiles ? "drifted" : "needs_install";
1510
1769
  }
1511
1770
  if (input.hasMissing) {
@@ -1517,18 +1776,21 @@ function summarizeRuntimeBridgeStatus(input) {
1517
1776
  if (input.state === "ready") {
1518
1777
  return input.compatibility === "migration_available"
1519
1778
  ? "Runtime bridge files are installed and healthy, but the project config still needs the current runtime_bridge contract migrated."
1520
- : "Runtime bridge files, autoload registration, and managed manifest all match the current GDH baseline.";
1779
+ : "Runtime and editor bridge files, project.godot registrations, and managed manifest all match the current GDH baseline.";
1521
1780
  }
1522
1781
  if (input.state === "needs_install") {
1523
- return `Runtime bridge is configured for "${input.runtimeBridge.projectPath}" but the managed addon files or autoload entry have not been installed yet.`;
1782
+ return `GDH bridge is configured for "${input.runtimeBridge.projectPath}" but the managed addon files or project.godot registrations have not been installed yet.`;
1524
1783
  }
1525
1784
  if (input.state === "drifted") {
1526
1785
  if (!input.autoloadRegistered) {
1527
- return "Runtime bridge addon files are installed, but GDHBridge is not registered yet. Enable the GDH Runtime Bridge plugin in the Godot editor so it can register the autoload itself.";
1786
+ return "GDH bridge addon files are installed, but GDHBridge is not registered as an autoload yet.";
1787
+ }
1788
+ if (!input.editorPluginRegistered) {
1789
+ return "GDH bridge addon files are installed, but the GDH editor plugin is not enabled in project.godot yet.";
1528
1790
  }
1529
1791
  return input.customEntryCount > 0
1530
- ? "Runtime bridge lifecycle detected drift in GDH-managed files or autoload registration while preserving project-owned bridge entries."
1531
- : "Runtime bridge lifecycle detected drift in GDH-managed files or autoload registration.";
1792
+ ? "GDH bridge lifecycle detected drift in GDH-managed files or project.godot registrations while preserving project-owned bridge entries."
1793
+ : "GDH bridge lifecycle detected drift in GDH-managed files or project.godot registrations.";
1532
1794
  }
1533
1795
  return "Runtime bridge lifecycle is blocked and cannot inspect the configured target project safely.";
1534
1796
  }
@@ -1548,13 +1810,79 @@ function hasAutoloadRegistration(content, runtimeBridge) {
1548
1810
  const normalizedValue = value.replace(/^"\*/, "").replace(/^"/, "").replace(/"$/, "");
1549
1811
  return normalizedValue === runtimeBridge.autoloadScriptPath;
1550
1812
  }
1813
+ function hasEditorPluginRegistration(content, runtimeBridge) {
1814
+ return readEditorPluginRegistrations(content).includes(editorPluginResourcePath(runtimeBridge));
1815
+ }
1816
+ function addEditorPluginRegistration(content, runtimeBridge) {
1817
+ const pluginPath = editorPluginResourcePath(runtimeBridge);
1818
+ const lines = content.split(/\r?\n/);
1819
+ const section = findSection(lines, "editor_plugins");
1820
+ const registrations = readEditorPluginRegistrations(content);
1821
+ const nextRegistrations = registrations.includes(pluginPath)
1822
+ ? registrations
1823
+ : [...registrations, pluginPath].sort();
1824
+ const entryLine = renderEditorPluginsEnabledLine(nextRegistrations);
1825
+ if (section === null) {
1826
+ const trailingBlank = lines.length > 0 && lines[lines.length - 1] === "" ? 1 : 0;
1827
+ const baseLines = trailingBlank ? lines.slice(0, -1) : lines;
1828
+ const needsSeparator = baseLines.length > 0 && baseLines[baseLines.length - 1]?.trim() !== "";
1829
+ return `${[
1830
+ ...baseLines,
1831
+ ...(needsSeparator ? [""] : []),
1832
+ "[editor_plugins]",
1833
+ "",
1834
+ entryLine,
1835
+ ].join("\n")}\n`;
1836
+ }
1837
+ const replacementIndex = lines.findIndex((line, index) => index > section.start && index < section.end && line.trim().startsWith("enabled="));
1838
+ const nextLines = replacementIndex >= 0
1839
+ ? lines.map((line, index) => (index === replacementIndex ? entryLine : line))
1840
+ : [...lines.slice(0, section.end), entryLine, ...lines.slice(section.end)];
1841
+ return `${nextLines.join("\n").replace(/\n+$/, "\n")}`;
1842
+ }
1843
+ function removeEditorPluginRegistration(content, runtimeBridge) {
1844
+ const pluginPath = editorPluginResourcePath(runtimeBridge);
1845
+ const lines = content.split(/\r?\n/);
1846
+ const section = findSection(lines, "editor_plugins");
1847
+ if (section === null)
1848
+ return `${lines.join("\n").replace(/\n+$/, "\n")}`;
1849
+ const registrations = readEditorPluginRegistrations(content).filter((entry) => entry !== pluginPath);
1850
+ const entryLine = renderEditorPluginsEnabledLine(registrations);
1851
+ const replacementIndex = lines.findIndex((line, index) => index > section.start && index < section.end && line.trim().startsWith("enabled="));
1852
+ if (replacementIndex < 0)
1853
+ return `${lines.join("\n").replace(/\n+$/, "\n")}`;
1854
+ const nextLines = lines.map((line, index) => (index === replacementIndex ? entryLine : line));
1855
+ return `${nextLines.join("\n").replace(/\n+$/, "\n")}`;
1856
+ }
1857
+ function readEditorPluginRegistrations(content) {
1858
+ const lines = content.split(/\r?\n/);
1859
+ const section = findSection(lines, "editor_plugins");
1860
+ if (section === null)
1861
+ return [];
1862
+ for (let index = section.start + 1; index < section.end; index += 1) {
1863
+ const line = lines[index]?.trim() ?? "";
1864
+ if (line.startsWith("enabled=")) {
1865
+ const entries = [...line.matchAll(/"([^"]+)"/gu)].map((match) => match[1] ?? "");
1866
+ return entries.filter((entry) => entry.length > 0);
1867
+ }
1868
+ }
1869
+ return [];
1870
+ }
1871
+ function renderEditorPluginsEnabledLine(entries) {
1872
+ return `enabled=PackedStringArray(${entries.map((entry) => JSON.stringify(entry)).join(", ")})`;
1873
+ }
1874
+ function editorPluginResourcePath(runtimeBridge) {
1875
+ return `res://${normalizePath(path.join(runtimeBridge.addonPath, "plugin.cfg"))}`;
1876
+ }
1551
1877
  function removeAutoloadRegistration(content, runtimeBridge) {
1552
1878
  const lines = content.split(/\r?\n/);
1553
1879
  const section = findSection(lines, "autoload");
1554
1880
  if (section === null) {
1555
1881
  return `${lines.join("\n").replace(/\n+$/, "\n")}`;
1556
1882
  }
1557
- const nextLines = lines.filter((_, index) => !(index > section.start && index < section.end && lines[index]?.startsWith(`${runtimeBridge.autoloadName}=`)));
1883
+ const nextLines = lines.filter((_, index) => !(index > section.start &&
1884
+ index < section.end &&
1885
+ lines[index]?.startsWith(`${runtimeBridge.autoloadName}=`)));
1558
1886
  return `${nextLines.join("\n").replace(/\n+$/, "\n")}`;
1559
1887
  }
1560
1888
  /**
@@ -1585,12 +1913,7 @@ function addAutoloadRegistration(content, runtimeBridge) {
1585
1913
  // section if the previous content does not already end with one.
1586
1914
  const baseLines = trailingBlank ? lines.slice(0, -1) : lines;
1587
1915
  const needsSeparator = baseLines.length > 0 && baseLines[baseLines.length - 1]?.trim() !== "";
1588
- const sectionLines = [
1589
- ...(needsSeparator ? [""] : []),
1590
- "[autoload]",
1591
- "",
1592
- entryLine,
1593
- ];
1916
+ const sectionLines = [...(needsSeparator ? [""] : []), "[autoload]", "", entryLine];
1594
1917
  return `${[...baseLines, ...sectionLines].join("\n")}\n`;
1595
1918
  }
1596
1919
  // Section exists. Replace any existing entry for this autoloadName, or