update_agents_structure.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Script to update the directory structure section in AGENTS.md.
  5. This script scans the repository and generates an updated directory structure
  6. that can be used to update the AGENTS.md file.
  7. """
  8. import os
  9. import re
  10. import sys
  11. # File descriptions for known files
  12. FILE_DESCRIPTIONS = {
  13. # Root level
  14. "run.py": "Direct run script",
  15. "install.sh": "One-click install script",
  16. "pyproject.toml": "Python project configuration",
  17. "setup.cfg": "Setup configuration",
  18. ".gitignore": "Git ignore rules",
  19. "LICENSE": "MIT License",
  20. "README.md": "Main README (Chinese)",
  21. "README.en.md": "Main README (English)",
  22. # ddns main files
  23. "ddns/__init__.py": "Package initialization and version info",
  24. "ddns/__main__.py": "Entry point for module execution",
  25. "ddns/cache.py": "Cache management",
  26. "ddns/ip.py": "IP address detection logic",
  27. # ddns/config
  28. "ddns/config/__init__.py": "",
  29. "ddns/config/cli.py": "Command-line argument parsing",
  30. "ddns/config/config.py": "Configuration loading and merging",
  31. "ddns/config/env.py": "Environment variable parsing",
  32. "ddns/config/file.py": "JSON file configuration",
  33. # ddns/provider
  34. "ddns/provider/__init__.py": "Provider registry",
  35. "ddns/provider/_base.py": "Abstract base classes (SimpleProvider, BaseProvider)",
  36. "ddns/provider/_signature.py": "HMAC signature utilities",
  37. "ddns/provider/alidns.py": "Alibaba Cloud DNS",
  38. "ddns/provider/aliesa.py": "Alibaba Cloud ESA",
  39. "ddns/provider/callback.py": "Custom webhook callbacks",
  40. "ddns/provider/cloudflare.py": "Cloudflare DNS",
  41. "ddns/provider/debug.py": "Debug provider",
  42. "ddns/provider/dnscom.py": "DNS.COM",
  43. "ddns/provider/dnspod.py": "DNSPod (China)",
  44. "ddns/provider/dnspod_com.py": "DNSPod International",
  45. "ddns/provider/edgeone.py": "Tencent EdgeOne",
  46. "ddns/provider/edgeone_dns.py": "Tencent EdgeOne DNS",
  47. "ddns/provider/he.py": "Hurricane Electric",
  48. "ddns/provider/huaweidns.py": "Huawei Cloud DNS",
  49. "ddns/provider/namesilo.py": "NameSilo",
  50. "ddns/provider/noip.py": "No-IP",
  51. "ddns/provider/tencentcloud.py": "Tencent Cloud DNS",
  52. # ddns/scheduler
  53. "ddns/scheduler/__init__.py": "",
  54. "ddns/scheduler/_base.py": "Base scheduler class",
  55. "ddns/scheduler/cron.py": "Cron-based scheduler (Linux/macOS)",
  56. "ddns/scheduler/launchd.py": "macOS launchd scheduler",
  57. "ddns/scheduler/schtasks.py": "Windows Task Scheduler",
  58. "ddns/scheduler/systemd.py": "Linux systemd timer",
  59. # ddns/util
  60. "ddns/util/__init__.py": "",
  61. "ddns/util/comment.py": "Comment handling",
  62. "ddns/util/fileio.py": "File I/O operations",
  63. "ddns/util/http.py": "HTTP client with proxy support",
  64. "ddns/util/try_run.py": "Safe command execution",
  65. # tests
  66. "tests/__init__.py": "Test initialization (path setup)",
  67. "tests/base_test.py": "Shared test utilities and base classes",
  68. "tests/README.md": "Testing documentation",
  69. # docker
  70. "docker/Dockerfile": "Main Dockerfile",
  71. "docker/glibc.Dockerfile": "glibc-based build",
  72. "docker/musl.Dockerfile": "musl-based build",
  73. "docker/entrypoint.sh": "Container entrypoint script",
  74. }
  75. def extract_current_structure(agents_content):
  76. # type: (str) -> str | None
  77. """Extract the current directory structure section from AGENTS.md."""
  78. # Find the directory structure section
  79. pattern = r"### Directory Structure\s*\n\n```text\n(.*?)```"
  80. match = re.search(pattern, agents_content, re.DOTALL)
  81. if match:
  82. return match.group(1).strip()
  83. return None
  84. def update_agents_structure(agents_content, new_structure):
  85. # type: (str, str) -> str
  86. """Update the directory structure section in AGENTS.md content."""
  87. pattern = r"(### Directory Structure\s*\n\n```text\n)(.*?)(```)"
  88. replacement = r"\g<1>" + new_structure + "\n" + r"\g<3>"
  89. return re.sub(pattern, replacement, agents_content, flags=re.DOTALL)
  90. def version_sort_key(filename):
  91. # type: (str) -> list
  92. """Sort key for version-named files like v2.json, v2.8.json, v4.0.json."""
  93. # Extract version number from filename (e.g., v2.8.json -> [2, 8])
  94. name = filename.rsplit(".", 1)[0] # Remove extension
  95. if name.startswith("v"):
  96. name = name[1:] # Remove 'v' prefix
  97. parts = name.split(".")
  98. result = []
  99. for part in parts:
  100. try:
  101. result.append(int(part))
  102. except ValueError:
  103. result.append(0)
  104. return result
  105. def get_sorted_files(directory, extensions=None, version_sort=False):
  106. # type: (str, list | None, bool) -> list
  107. """Get sorted list of files from a directory."""
  108. result = []
  109. if os.path.isdir(directory):
  110. for f in os.listdir(directory):
  111. if extensions:
  112. if any(f.endswith(ext) for ext in extensions):
  113. result.append(f)
  114. else:
  115. result.append(f)
  116. if version_sort:
  117. result.sort(key=version_sort_key)
  118. else:
  119. result.sort()
  120. return result
  121. def generate_full_structure(repo_root):
  122. # type: (str) -> str
  123. """Generate the full directory structure matching AGENTS.md format."""
  124. lines = []
  125. # Root
  126. lines.append("DDNS/")
  127. # .github section
  128. lines.append("\u251c\u2500\u2500 .github/ # GitHub configuration")
  129. lines.append("\u2502 \u251c\u2500\u2500 workflows/ # CI/CD workflows (build, publish, test)")
  130. lines.append("\u2502 \u251c\u2500\u2500 instructions/ # Agent instructions (python.instructions.md)")
  131. lines.append("\u2502 \u2514\u2500\u2500 copilot-instructions.md # GitHub Copilot instructions")
  132. lines.append("\u2502")
  133. # ddns section
  134. lines.append("\u251c\u2500\u2500 ddns/ # Main application code")
  135. lines.append("\u2502 \u251c\u2500\u2500 __init__.py # Package initialization and version info")
  136. lines.append("\u2502 \u251c\u2500\u2500 __main__.py # Entry point for module execution")
  137. lines.append("\u2502 \u251c\u2500\u2500 cache.py # Cache management")
  138. lines.append("\u2502 \u251c\u2500\u2500 ip.py # IP address detection logic")
  139. lines.append("\u2502 \u2502")
  140. # ddns/config
  141. lines.append("\u2502 \u251c\u2500\u2500 config/ # Configuration management")
  142. lines.append("\u2502 \u2502 \u251c\u2500\u2500 __init__.py")
  143. lines.append("\u2502 \u2502 \u251c\u2500\u2500 cli.py # Command-line argument parsing")
  144. lines.append("\u2502 \u2502 \u251c\u2500\u2500 config.py # Configuration loading and merging")
  145. lines.append("\u2502 \u2502 \u251c\u2500\u2500 env.py # Environment variable parsing")
  146. lines.append("\u2502 \u2502 \u2514\u2500\u2500 file.py # JSON file configuration")
  147. lines.append("\u2502 \u2502")
  148. # ddns/provider - dynamic generation
  149. lines.append("\u2502 \u251c\u2500\u2500 provider/ # DNS provider implementations")
  150. provider_dir = os.path.join(repo_root, "ddns", "provider")
  151. provider_files = get_sorted_files(provider_dir, [".py"])
  152. for i, f in enumerate(provider_files):
  153. filepath = "ddns/provider/" + f
  154. desc = FILE_DESCRIPTIONS.get(filepath, "")
  155. is_last = i == len(provider_files) - 1
  156. prefix = "\u2502 \u2502 \u2514\u2500\u2500 " if is_last else "\u2502 \u2502 \u251c\u2500\u2500 "
  157. if desc:
  158. padded_name = f.ljust(20)
  159. lines.append(prefix + padded_name + "# " + desc)
  160. else:
  161. # For unknown providers, generate a description
  162. if f.startswith("_"):
  163. if f == "__init__.py":
  164. lines.append(prefix + f)
  165. else:
  166. lines.append(prefix + f)
  167. else:
  168. name = f[:-3] # Remove .py
  169. lines.append(prefix + f.ljust(20) + "# " + name.title() + " DNS provider")
  170. lines.append("\u2502 \u2502")
  171. # ddns/scheduler
  172. lines.append("\u2502 \u251c\u2500\u2500 scheduler/ # Task scheduling implementations")
  173. lines.append("\u2502 \u2502 \u251c\u2500\u2500 __init__.py")
  174. lines.append("\u2502 \u2502 \u251c\u2500\u2500 _base.py # Base scheduler class")
  175. lines.append("\u2502 \u2502 \u251c\u2500\u2500 cron.py # Cron-based scheduler (Linux/macOS)")
  176. lines.append("\u2502 \u2502 \u251c\u2500\u2500 launchd.py # macOS launchd scheduler")
  177. lines.append("\u2502 \u2502 \u251c\u2500\u2500 schtasks.py # Windows Task Scheduler")
  178. lines.append("\u2502 \u2502 \u2514\u2500\u2500 systemd.py # Linux systemd timer")
  179. lines.append("\u2502 \u2502")
  180. # ddns/util
  181. lines.append("\u2502 \u2514\u2500\u2500 util/ # Utility modules")
  182. lines.append("\u2502 \u251c\u2500\u2500 __init__.py")
  183. lines.append("\u2502 \u251c\u2500\u2500 comment.py # Comment handling")
  184. lines.append("\u2502 \u251c\u2500\u2500 fileio.py # File I/O operations")
  185. lines.append("\u2502 \u251c\u2500\u2500 http.py # HTTP client with proxy support")
  186. lines.append("\u2502 \u2514\u2500\u2500 try_run.py # Safe command execution")
  187. lines.append("\u2502")
  188. # tests section
  189. lines.append("\u251c\u2500\u2500 tests/ # Unit tests")
  190. lines.append("\u2502 \u251c\u2500\u2500 __init__.py # Test initialization (path setup)")
  191. lines.append("\u2502 \u251c\u2500\u2500 base_test.py # Shared test utilities and base classes")
  192. lines.append("\u2502 \u251c\u2500\u2500 README.md # Testing documentation")
  193. lines.append("\u2502 \u251c\u2500\u2500 config/ # Test configuration files")
  194. lines.append("\u2502 \u251c\u2500\u2500 scripts/ # Test helper scripts")
  195. lines.append("\u2502 \u251c\u2500\u2500 test_cache.py # Cache tests")
  196. lines.append("\u2502 \u251c\u2500\u2500 test_config_*.py # Configuration tests")
  197. lines.append("\u2502 \u251c\u2500\u2500 test_ip.py # IP detection tests")
  198. lines.append("\u2502 \u251c\u2500\u2500 test_provider_*.py # Provider-specific tests")
  199. lines.append("\u2502 \u251c\u2500\u2500 test_scheduler_*.py # Scheduler tests")
  200. lines.append("\u2502 \u2514\u2500\u2500 test_util_*.py # Utility tests")
  201. lines.append("\u2502")
  202. # doc section
  203. lines.append("\u251c\u2500\u2500 doc/ # Documentation")
  204. lines.append("\u2502 \u251c\u2500\u2500 config/ # Configuration documentation")
  205. lines.append("\u2502 \u2502 \u251c\u2500\u2500 cli.md # CLI usage (Chinese)")
  206. lines.append("\u2502 \u2502 \u251c\u2500\u2500 cli.en.md # CLI usage (English)")
  207. lines.append("\u2502 \u2502 \u251c\u2500\u2500 env.md # Environment variables (Chinese)")
  208. lines.append("\u2502 \u2502 \u251c\u2500\u2500 env.en.md # Environment variables (English)")
  209. lines.append("\u2502 \u2502 \u251c\u2500\u2500 json.md # JSON config (Chinese)")
  210. lines.append("\u2502 \u2502 \u2514\u2500\u2500 json.en.md # JSON config (English)")
  211. lines.append("\u2502 \u2502")
  212. lines.append("\u2502 \u251c\u2500\u2500 dev/ # Developer documentation")
  213. lines.append("\u2502 \u2502 \u251c\u2500\u2500 provider.md # Provider development guide (Chinese)")
  214. lines.append("\u2502 \u2502 \u251c\u2500\u2500 provider.en.md # Provider development guide (English)")
  215. lines.append("\u2502 \u2502 \u251c\u2500\u2500 config.md # Config system (Chinese)")
  216. lines.append("\u2502 \u2502 \u2514\u2500\u2500 config.en.md # Config system (English)")
  217. lines.append("\u2502 \u2502")
  218. lines.append("\u2502 \u251c\u2500\u2500 providers/ # Provider-specific documentation")
  219. lines.append("\u2502 \u2502 \u251c\u2500\u2500 README.md # Provider list (Chinese)")
  220. lines.append("\u2502 \u2502 \u251c\u2500\u2500 README.en.md # Provider list (English)")
  221. lines.append("\u2502 \u2502 \u251c\u2500\u2500 alidns.md # AliDNS guide (Chinese)")
  222. lines.append("\u2502 \u2502 \u251c\u2500\u2500 alidns.en.md # AliDNS guide (English)")
  223. lines.append(
  224. "\u2502 \u2502 \u2514\u2500\u2500 ... # Other providers (Chinese & English versions)"
  225. )
  226. lines.append("\u2502 \u2502")
  227. lines.append("\u2502 \u251c\u2500\u2500 docker.md # Docker documentation (Chinese)")
  228. lines.append("\u2502 \u251c\u2500\u2500 docker.en.md # Docker documentation (English)")
  229. lines.append("\u2502 \u251c\u2500\u2500 install.md # Installation guide (Chinese)")
  230. lines.append("\u2502 \u251c\u2500\u2500 install.en.md # Installation guide (English)")
  231. lines.append("\u2502 \u2514\u2500\u2500 img/ # Images and diagrams")
  232. lines.append("\u2502")
  233. # docker section
  234. lines.append("\u251c\u2500\u2500 docker/ # Docker configuration")
  235. lines.append("\u2502 \u251c\u2500\u2500 Dockerfile # Main Dockerfile")
  236. lines.append("\u2502 \u251c\u2500\u2500 glibc.Dockerfile # glibc-based build")
  237. lines.append("\u2502 \u251c\u2500\u2500 musl.Dockerfile # musl-based build")
  238. lines.append("\u2502 \u2514\u2500\u2500 entrypoint.sh # Container entrypoint script")
  239. lines.append("\u2502")
  240. # schema section - dynamic generation
  241. lines.append("\u251c\u2500\u2500 schema/ # JSON schemas")
  242. schema_dir = os.path.join(repo_root, "schema")
  243. schema_files = get_sorted_files(schema_dir, [".json"], version_sort=True)
  244. for i, f in enumerate(schema_files):
  245. is_last = i == len(schema_files) - 1
  246. prefix = "\u2502 \u2514\u2500\u2500 " if is_last else "\u2502 \u251c\u2500\u2500 "
  247. version = f[:-5] # Remove .json
  248. if version == "v2":
  249. desc = "Legacy schema v2"
  250. elif version == "v2.8":
  251. desc = "Legacy schema v2.8"
  252. elif version == "v4.0":
  253. desc = "Previous schema v4.0"
  254. elif version == "v4.1":
  255. desc = "Latest schema v4.1"
  256. else:
  257. desc = "Schema " + version
  258. padded_name = f.ljust(24)
  259. lines.append(prefix + padded_name + "# " + desc)
  260. lines.append("\u2502")
  261. # Root files
  262. lines.append("\u251c\u2500\u2500 run.py # Direct run script")
  263. lines.append("\u251c\u2500\u2500 install.sh # One-click install script")
  264. lines.append("\u251c\u2500\u2500 pyproject.toml # Python project configuration")
  265. lines.append("\u251c\u2500\u2500 setup.cfg # Setup configuration")
  266. lines.append("\u251c\u2500\u2500 .gitignore # Git ignore rules")
  267. lines.append("\u251c\u2500\u2500 LICENSE # MIT License")
  268. lines.append("\u251c\u2500\u2500 README.md # Main README (Chinese)")
  269. lines.append("\u2514\u2500\u2500 README.en.md # Main README (English)")
  270. return "\n".join(lines)
  271. def main():
  272. # type: () -> None
  273. """Main function to update AGENTS.md directory structure."""
  274. # Determine repository root
  275. script_dir = os.path.dirname(os.path.abspath(__file__))
  276. repo_root = os.path.dirname(os.path.dirname(script_dir))
  277. agents_file = os.path.join(repo_root, "AGENTS.md")
  278. if not os.path.exists(agents_file):
  279. print("Error: AGENTS.md not found at " + agents_file)
  280. sys.exit(1)
  281. # Read current AGENTS.md
  282. with open(agents_file, "r", encoding="utf-8") as f:
  283. agents_content = f.read()
  284. # Extract current structure
  285. current_structure = extract_current_structure(agents_content)
  286. if current_structure is None:
  287. print("Error: Could not find directory structure section in AGENTS.md")
  288. sys.exit(1)
  289. # Generate new structure
  290. new_structure = generate_full_structure(repo_root)
  291. # Check if structure has changed
  292. if current_structure.strip() == new_structure.strip():
  293. print("No changes detected in directory structure.")
  294. sys.exit(0)
  295. # Update AGENTS.md content
  296. updated_content = update_agents_structure(agents_content, new_structure)
  297. # Write updated content
  298. with open(agents_file, "w", encoding="utf-8") as f:
  299. f.write(updated_content)
  300. print("AGENTS.md directory structure has been updated.")
  301. print("\nChanges detected:")
  302. print("Old structure lines: " + str(len(current_structure.strip().split("\n"))))
  303. print("New structure lines: " + str(len(new_structure.strip().split("\n"))))
  304. sys.exit(0)
  305. if __name__ == "__main__":
  306. main()