Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

resolve_config.py 5.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. #!/usr/bin/env python3
  2. """
  3. Resolve BMad's central config using four-layer TOML merge.
  4. Reads from four layers (highest priority last):
  5. 1. {project-root}/_bmad/config.toml (installer-owned team)
  6. 2. {project-root}/_bmad/config.user.toml (installer-owned user)
  7. 3. {project-root}/_bmad/custom/config.toml (human-authored team, committed)
  8. 4. {project-root}/_bmad/custom/config.user.toml (human-authored user, gitignored)
  9. Outputs merged JSON to stdout. Errors go to stderr.
  10. Requires Python 3.11+ (uses stdlib `tomllib`). No `uv`, no `pip install`,
  11. no virtualenv — plain `python3` is sufficient.
  12. python3 resolve_config.py --project-root /abs/path/to/project
  13. python3 resolve_config.py --project-root ... --key core
  14. python3 resolve_config.py --project-root ... --key agents
  15. Merge rules (same as resolve_customization.py):
  16. - Scalars: override wins
  17. - Tables: deep merge
  18. - Arrays of tables where every item shares `code` or `id`: merge by that key
  19. - All other arrays: append
  20. """
  21. import argparse
  22. import json
  23. import sys
  24. from pathlib import Path
  25. try:
  26. import tomllib
  27. except ImportError:
  28. sys.stderr.write(
  29. "error: Python 3.11+ is required (stdlib `tomllib` not found).\n"
  30. )
  31. sys.exit(3)
  32. _MISSING = object()
  33. _KEYED_MERGE_FIELDS = ("code", "id")
  34. def load_toml(file_path: Path, required: bool = False) -> dict:
  35. if not file_path.exists():
  36. if required:
  37. sys.stderr.write(f"error: required config file not found: {file_path}\n")
  38. sys.exit(1)
  39. return {}
  40. try:
  41. with file_path.open("rb") as f:
  42. parsed = tomllib.load(f)
  43. if not isinstance(parsed, dict):
  44. return {}
  45. return parsed
  46. except tomllib.TOMLDecodeError as error:
  47. level = "error" if required else "warning"
  48. sys.stderr.write(f"{level}: failed to parse {file_path}: {error}\n")
  49. if required:
  50. sys.exit(1)
  51. return {}
  52. except OSError as error:
  53. level = "error" if required else "warning"
  54. sys.stderr.write(f"{level}: failed to read {file_path}: {error}\n")
  55. if required:
  56. sys.exit(1)
  57. return {}
  58. def _detect_keyed_merge_field(items):
  59. if not items or not all(isinstance(item, dict) for item in items):
  60. return None
  61. for candidate in _KEYED_MERGE_FIELDS:
  62. if all(item.get(candidate) is not None for item in items):
  63. return candidate
  64. return None
  65. def _merge_by_key(base, override, key_name):
  66. result = []
  67. index_by_key = {}
  68. for item in base:
  69. if not isinstance(item, dict):
  70. continue
  71. if item.get(key_name) is not None:
  72. index_by_key[item[key_name]] = len(result)
  73. result.append(dict(item))
  74. for item in override:
  75. if not isinstance(item, dict):
  76. result.append(item)
  77. continue
  78. key = item.get(key_name)
  79. if key is not None and key in index_by_key:
  80. result[index_by_key[key]] = dict(item)
  81. else:
  82. if key is not None:
  83. index_by_key[key] = len(result)
  84. result.append(dict(item))
  85. return result
  86. def _merge_arrays(base, override):
  87. base_arr = base if isinstance(base, list) else []
  88. override_arr = override if isinstance(override, list) else []
  89. keyed_field = _detect_keyed_merge_field(base_arr + override_arr)
  90. if keyed_field:
  91. return _merge_by_key(base_arr, override_arr, keyed_field)
  92. return base_arr + override_arr
  93. def deep_merge(base, override):
  94. if isinstance(base, dict) and isinstance(override, dict):
  95. result = dict(base)
  96. for key, over_val in override.items():
  97. if key in result:
  98. result[key] = deep_merge(result[key], over_val)
  99. else:
  100. result[key] = over_val
  101. return result
  102. if isinstance(base, list) and isinstance(override, list):
  103. return _merge_arrays(base, override)
  104. return override
  105. def extract_key(data, dotted_key: str):
  106. parts = dotted_key.split(".")
  107. current = data
  108. for part in parts:
  109. if isinstance(current, dict) and part in current:
  110. current = current[part]
  111. else:
  112. return _MISSING
  113. return current
  114. def main():
  115. parser = argparse.ArgumentParser(
  116. description="Resolve BMad central config using four-layer TOML merge.",
  117. )
  118. parser.add_argument(
  119. "--project-root", "-p", required=True,
  120. help="Absolute path to the project root (contains _bmad/)",
  121. )
  122. parser.add_argument(
  123. "--key", "-k", action="append", default=[],
  124. help="Dotted field path to resolve (repeatable). Omit for full dump.",
  125. )
  126. args = parser.parse_args()
  127. project_root = Path(args.project_root).resolve()
  128. bmad_dir = project_root / "_bmad"
  129. base_team = load_toml(bmad_dir / "config.toml", required=True)
  130. base_user = load_toml(bmad_dir / "config.user.toml")
  131. custom_team = load_toml(bmad_dir / "custom" / "config.toml")
  132. custom_user = load_toml(bmad_dir / "custom" / "config.user.toml")
  133. merged = deep_merge(base_team, base_user)
  134. merged = deep_merge(merged, custom_team)
  135. merged = deep_merge(merged, custom_user)
  136. if args.key:
  137. output = {}
  138. for key in args.key:
  139. value = extract_key(merged, key)
  140. if value is not _MISSING:
  141. output[key] = value
  142. else:
  143. output = merged
  144. sys.stdout.write(json.dumps(output, indent=2, ensure_ascii=False) + "\n")
  145. if __name__ == "__main__":
  146. main()