install_hooks.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """hooks.json 占位符替换安装器。
  4. 将 hooks.json 中的 {{PYTHON_BIN}} 和 {{HOOKS_DIR}} 占位符替换为真实路径,
  5. 生成可直接加载的 hooks 配置,降低对运行时占位符替换能力的依赖。
  6. 用法:
  7. # 1. 自动探测(推荐):用当前 Python 解释器 + 脚本所在目录作为 HOOKS_DIR
  8. python install_hooks.py
  9. # 2. 通过命令行参数指定
  10. python install_hooks.py --python-bin /usr/bin/python3 --hooks-dir /path/to/hooks
  11. # 3. 通过环境变量指定
  12. PYTHON_BIN=/usr/bin/python3 HOOKS_DIR=/path/to/hooks python install_hooks.py
  13. # 4. 指定输出路径(默认输出到 hooks.installed.json,不覆盖原模板)
  14. python install_hooks.py --output /path/to/hooks.final.json
  15. # 5. 原地覆盖(谨慎使用)
  16. python install_hooks.py --in-place
  17. 优先级:命令行参数 > 环境变量 > 自动探测
  18. """
  19. import argparse
  20. import json
  21. import os
  22. import shutil
  23. import sys
  24. def main():
  25. parser = argparse.ArgumentParser(
  26. description="hooks.json 占位符替换安装器",
  27. formatter_class=argparse.RawDescriptionHelpFormatter,
  28. )
  29. parser.add_argument(
  30. "--python-bin",
  31. default=os.environ.get("PYTHON_BIN", ""),
  32. help="Python 解释器路径(默认:当前 sys.executable,或环境变量 PYTHON_BIN)",
  33. )
  34. parser.add_argument(
  35. "--hooks-dir",
  36. default=os.environ.get("HOOKS_DIR", ""),
  37. help="hook 脚本目录(默认:本脚本所在目录,或环境变量 HOOKS_DIR)",
  38. )
  39. parser.add_argument(
  40. "--input",
  41. default=None,
  42. help="输入 hooks.json 模板路径(默认:本脚本上级目录的 hooks.json)",
  43. )
  44. parser.add_argument(
  45. "--output",
  46. default=None,
  47. help="输出路径(默认:与输入同目录的 hooks.installed.json,不覆盖原模板)",
  48. )
  49. parser.add_argument(
  50. "--in-place",
  51. action="store_true",
  52. help="原地覆盖输入文件(谨慎使用,会丢失模板占位符;自动备份为 .bak)",
  53. )
  54. args = parser.parse_args()
  55. script_dir = os.path.dirname(os.path.abspath(__file__))
  56. # 解析 Python 解释器路径
  57. python_bin = args.python_bin or sys.executable
  58. if not os.path.isabs(python_bin):
  59. python_bin = os.path.abspath(python_bin)
  60. if not os.path.isfile(python_bin):
  61. print("[INSTALL-HOOKS] 错误:Python 解释器不存在: %s" % python_bin, file=sys.stderr)
  62. return 1
  63. # 解析 hooks 目录
  64. hooks_dir = args.hooks_dir or script_dir
  65. if not os.path.isabs(hooks_dir):
  66. hooks_dir = os.path.abspath(hooks_dir)
  67. if not os.path.isdir(hooks_dir):
  68. print("[INSTALL-HOOKS] 错误:hooks 目录不存在: %s" % hooks_dir, file=sys.stderr)
  69. return 1
  70. # 解析输入路径(默认上级目录的 hooks.json)
  71. input_path = args.input or os.path.abspath(os.path.join(script_dir, "..", "hooks.json"))
  72. if not os.path.isfile(input_path):
  73. print("[INSTALL-HOOKS] 错误:hooks.json 模板不存在: %s" % input_path, file=sys.stderr)
  74. return 1
  75. # 解析输出路径
  76. if args.in_place:
  77. output_path = input_path
  78. else:
  79. output_path = args.output or os.path.join(
  80. os.path.dirname(input_path), "hooks.installed.json"
  81. )
  82. output_path = os.path.abspath(output_path)
  83. # 读取模板
  84. try:
  85. with open(input_path, encoding="utf-8") as f:
  86. raw = f.read()
  87. except OSError as e:
  88. print("[INSTALL-HOOKS] 错误:读取模板失败: %s" % e, file=sys.stderr)
  89. return 1
  90. # 替换占位符(Windows 路径反斜杠在 JSON 字符串中需转义)
  91. python_bin_escaped = python_bin.replace("\\", "\\\\")
  92. hooks_dir_escaped = hooks_dir.replace("\\", "\\\\")
  93. replacements = {
  94. "{{PYTHON_BIN}}": python_bin_escaped,
  95. "{{HOOKS_DIR}}": hooks_dir_escaped,
  96. }
  97. result = raw
  98. for placeholder, value in replacements.items():
  99. result = result.replace(placeholder, value)
  100. # 验证 JSON 合法性
  101. try:
  102. json.loads(result)
  103. except json.JSONDecodeError as e:
  104. print("[INSTALL-HOOKS] 错误:替换后 JSON 解析失败: %s" % e, file=sys.stderr)
  105. print("[INSTALL-HOOKS] 替换结果片段:", file=sys.stderr)
  106. print(result[:500], file=sys.stderr)
  107. return 1
  108. # 原地覆盖时先备份
  109. if args.in_place:
  110. bak = input_path + ".bak"
  111. try:
  112. shutil.copy2(input_path, bak)
  113. print("[INSTALL-HOOKS] 已备份原模板到: %s" % bak)
  114. except OSError as e:
  115. print("[INSTALL-HOOKS] 警告:备份失败: %s" % e, file=sys.stderr)
  116. # 写入输出
  117. try:
  118. with open(output_path, "w", encoding="utf-8") as f:
  119. f.write(result)
  120. except OSError as e:
  121. print("[INSTALL-HOOKS] 错误:写入输出失败: %s" % e, file=sys.stderr)
  122. return 1
  123. # 摘要
  124. print("[INSTALL-HOOKS] 安装成功")
  125. print(" 模板: %s" % input_path)
  126. print(" 输出: %s" % output_path)
  127. print(" Python: %s" % python_bin)
  128. print(" Hooks Dir: %s" % hooks_dir)
  129. if not args.in_place:
  130. print("")
  131. print("下一步:将 %s 接入你的 Agent 运行时 hooks 配置入口" % output_path)
  132. return 0
  133. if __name__ == "__main__":
  134. sys.exit(main())