| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- Script to update the directory structure section in AGENTS.md.
- This script scans the repository and generates an updated directory structure
- that can be used to update the AGENTS.md file.
- """
- import os
- import re
- import sys
- # File descriptions for known files
- FILE_DESCRIPTIONS = {
- # Root level
- "run.py": "Direct run script",
- "install.sh": "One-click install script",
- "pyproject.toml": "Python project configuration",
- "setup.cfg": "Setup configuration",
- ".gitignore": "Git ignore rules",
- "LICENSE": "MIT License",
- "README.md": "Main README (Chinese)",
- "README.en.md": "Main README (English)",
- # ddns main files
- "ddns/__init__.py": "Package initialization and version info",
- "ddns/__main__.py": "Entry point for module execution",
- "ddns/cache.py": "Cache management",
- "ddns/ip.py": "IP address detection logic",
- # ddns/config
- "ddns/config/__init__.py": "",
- "ddns/config/cli.py": "Command-line argument parsing",
- "ddns/config/config.py": "Configuration loading and merging",
- "ddns/config/env.py": "Environment variable parsing",
- "ddns/config/file.py": "JSON file configuration",
- # ddns/provider
- "ddns/provider/__init__.py": "Provider registry",
- "ddns/provider/_base.py": "Abstract base classes (SimpleProvider, BaseProvider)",
- "ddns/provider/_signature.py": "HMAC signature utilities",
- "ddns/provider/alidns.py": "Alibaba Cloud DNS",
- "ddns/provider/aliesa.py": "Alibaba Cloud ESA",
- "ddns/provider/callback.py": "Custom webhook callbacks",
- "ddns/provider/cloudflare.py": "Cloudflare DNS",
- "ddns/provider/debug.py": "Debug provider",
- "ddns/provider/dnscom.py": "DNS.COM",
- "ddns/provider/dnspod.py": "DNSPod (China)",
- "ddns/provider/dnspod_com.py": "DNSPod International",
- "ddns/provider/edgeone.py": "Tencent EdgeOne",
- "ddns/provider/edgeone_dns.py": "Tencent EdgeOne DNS",
- "ddns/provider/he.py": "Hurricane Electric",
- "ddns/provider/huaweidns.py": "Huawei Cloud DNS",
- "ddns/provider/namesilo.py": "NameSilo",
- "ddns/provider/noip.py": "No-IP",
- "ddns/provider/tencentcloud.py": "Tencent Cloud DNS",
- # ddns/scheduler
- "ddns/scheduler/__init__.py": "",
- "ddns/scheduler/_base.py": "Base scheduler class",
- "ddns/scheduler/cron.py": "Cron-based scheduler (Linux/macOS)",
- "ddns/scheduler/launchd.py": "macOS launchd scheduler",
- "ddns/scheduler/schtasks.py": "Windows Task Scheduler",
- "ddns/scheduler/systemd.py": "Linux systemd timer",
- # ddns/util
- "ddns/util/__init__.py": "",
- "ddns/util/comment.py": "Comment handling",
- "ddns/util/fileio.py": "File I/O operations",
- "ddns/util/http.py": "HTTP client with proxy support",
- "ddns/util/try_run.py": "Safe command execution",
- # tests
- "tests/__init__.py": "Test initialization (path setup)",
- "tests/base_test.py": "Shared test utilities and base classes",
- "tests/README.md": "Testing documentation",
- # docker
- "docker/Dockerfile": "Main Dockerfile",
- "docker/glibc.Dockerfile": "glibc-based build",
- "docker/musl.Dockerfile": "musl-based build",
- "docker/entrypoint.sh": "Container entrypoint script",
- }
- def extract_current_structure(agents_content):
- # type: (str) -> str | None
- """Extract the current directory structure section from AGENTS.md."""
- # Find the directory structure section
- pattern = r"### Directory Structure\s*\n\n```text\n(.*?)```"
- match = re.search(pattern, agents_content, re.DOTALL)
- if match:
- return match.group(1).strip()
- return None
- def update_agents_structure(agents_content, new_structure):
- # type: (str, str) -> str
- """Update the directory structure section in AGENTS.md content."""
- pattern = r"(### Directory Structure\s*\n\n```text\n)(.*?)(```)"
- replacement = r"\g<1>" + new_structure + "\n" + r"\g<3>"
- return re.sub(pattern, replacement, agents_content, flags=re.DOTALL)
- def version_sort_key(filename):
- # type: (str) -> list
- """Sort key for version-named files like v2.json, v2.8.json, v4.0.json."""
- # Extract version number from filename (e.g., v2.8.json -> [2, 8])
- name = filename.rsplit(".", 1)[0] # Remove extension
- if name.startswith("v"):
- name = name[1:] # Remove 'v' prefix
- parts = name.split(".")
- result = []
- for part in parts:
- try:
- result.append(int(part))
- except ValueError:
- result.append(0)
- return result
- def get_sorted_files(directory, extensions=None, version_sort=False):
- # type: (str, list | None, bool) -> list
- """Get sorted list of files from a directory."""
- result = []
- if os.path.isdir(directory):
- for f in os.listdir(directory):
- if extensions:
- if any(f.endswith(ext) for ext in extensions):
- result.append(f)
- else:
- result.append(f)
- if version_sort:
- result.sort(key=version_sort_key)
- else:
- result.sort()
- return result
- def generate_full_structure(repo_root):
- # type: (str) -> str
- """Generate the full directory structure matching AGENTS.md format."""
- lines = []
- # Root
- lines.append("DDNS/")
- # .github section
- lines.append("\u251c\u2500\u2500 .github/ # GitHub configuration")
- lines.append("\u2502 \u251c\u2500\u2500 workflows/ # CI/CD workflows (build, publish, test)")
- lines.append("\u2502 \u251c\u2500\u2500 instructions/ # Agent instructions (python.instructions.md)")
- lines.append("\u2502 \u2514\u2500\u2500 copilot-instructions.md # GitHub Copilot instructions")
- lines.append("\u2502")
- # ddns section
- lines.append("\u251c\u2500\u2500 ddns/ # Main application code")
- lines.append("\u2502 \u251c\u2500\u2500 __init__.py # Package initialization and version info")
- lines.append("\u2502 \u251c\u2500\u2500 __main__.py # Entry point for module execution")
- lines.append("\u2502 \u251c\u2500\u2500 cache.py # Cache management")
- lines.append("\u2502 \u251c\u2500\u2500 ip.py # IP address detection logic")
- lines.append("\u2502 \u2502")
- # ddns/config
- lines.append("\u2502 \u251c\u2500\u2500 config/ # Configuration management")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 __init__.py")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 cli.py # Command-line argument parsing")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 config.py # Configuration loading and merging")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 env.py # Environment variable parsing")
- lines.append("\u2502 \u2502 \u2514\u2500\u2500 file.py # JSON file configuration")
- lines.append("\u2502 \u2502")
- # ddns/provider - dynamic generation
- lines.append("\u2502 \u251c\u2500\u2500 provider/ # DNS provider implementations")
- provider_dir = os.path.join(repo_root, "ddns", "provider")
- provider_files = get_sorted_files(provider_dir, [".py"])
- for i, f in enumerate(provider_files):
- filepath = "ddns/provider/" + f
- desc = FILE_DESCRIPTIONS.get(filepath, "")
- is_last = i == len(provider_files) - 1
- prefix = "\u2502 \u2502 \u2514\u2500\u2500 " if is_last else "\u2502 \u2502 \u251c\u2500\u2500 "
- if desc:
- padded_name = f.ljust(20)
- lines.append(prefix + padded_name + "# " + desc)
- else:
- # For unknown providers, generate a description
- if f.startswith("_"):
- if f == "__init__.py":
- lines.append(prefix + f)
- else:
- lines.append(prefix + f)
- else:
- name = f[:-3] # Remove .py
- lines.append(prefix + f.ljust(20) + "# " + name.title() + " DNS provider")
- lines.append("\u2502 \u2502")
- # ddns/scheduler
- lines.append("\u2502 \u251c\u2500\u2500 scheduler/ # Task scheduling implementations")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 __init__.py")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 _base.py # Base scheduler class")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 cron.py # Cron-based scheduler (Linux/macOS)")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 launchd.py # macOS launchd scheduler")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 schtasks.py # Windows Task Scheduler")
- lines.append("\u2502 \u2502 \u2514\u2500\u2500 systemd.py # Linux systemd timer")
- lines.append("\u2502 \u2502")
- # ddns/util
- lines.append("\u2502 \u2514\u2500\u2500 util/ # Utility modules")
- lines.append("\u2502 \u251c\u2500\u2500 __init__.py")
- lines.append("\u2502 \u251c\u2500\u2500 comment.py # Comment handling")
- lines.append("\u2502 \u251c\u2500\u2500 fileio.py # File I/O operations")
- lines.append("\u2502 \u251c\u2500\u2500 http.py # HTTP client with proxy support")
- lines.append("\u2502 \u2514\u2500\u2500 try_run.py # Safe command execution")
- lines.append("\u2502")
- # tests section
- lines.append("\u251c\u2500\u2500 tests/ # Unit tests")
- lines.append("\u2502 \u251c\u2500\u2500 __init__.py # Test initialization (path setup)")
- lines.append("\u2502 \u251c\u2500\u2500 base_test.py # Shared test utilities and base classes")
- lines.append("\u2502 \u251c\u2500\u2500 README.md # Testing documentation")
- lines.append("\u2502 \u251c\u2500\u2500 config/ # Test configuration files")
- lines.append("\u2502 \u251c\u2500\u2500 scripts/ # Test helper scripts")
- lines.append("\u2502 \u251c\u2500\u2500 test_cache.py # Cache tests")
- lines.append("\u2502 \u251c\u2500\u2500 test_config_*.py # Configuration tests")
- lines.append("\u2502 \u251c\u2500\u2500 test_ip.py # IP detection tests")
- lines.append("\u2502 \u251c\u2500\u2500 test_provider_*.py # Provider-specific tests")
- lines.append("\u2502 \u251c\u2500\u2500 test_scheduler_*.py # Scheduler tests")
- lines.append("\u2502 \u2514\u2500\u2500 test_util_*.py # Utility tests")
- lines.append("\u2502")
- # doc section
- lines.append("\u251c\u2500\u2500 doc/ # Documentation")
- lines.append("\u2502 \u251c\u2500\u2500 config/ # Configuration documentation")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 cli.md # CLI usage (Chinese)")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 cli.en.md # CLI usage (English)")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 env.md # Environment variables (Chinese)")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 env.en.md # Environment variables (English)")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 json.md # JSON config (Chinese)")
- lines.append("\u2502 \u2502 \u2514\u2500\u2500 json.en.md # JSON config (English)")
- lines.append("\u2502 \u2502")
- lines.append("\u2502 \u251c\u2500\u2500 dev/ # Developer documentation")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 provider.md # Provider development guide (Chinese)")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 provider.en.md # Provider development guide (English)")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 config.md # Config system (Chinese)")
- lines.append("\u2502 \u2502 \u2514\u2500\u2500 config.en.md # Config system (English)")
- lines.append("\u2502 \u2502")
- lines.append("\u2502 \u251c\u2500\u2500 providers/ # Provider-specific documentation")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 README.md # Provider list (Chinese)")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 README.en.md # Provider list (English)")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 alidns.md # AliDNS guide (Chinese)")
- lines.append("\u2502 \u2502 \u251c\u2500\u2500 alidns.en.md # AliDNS guide (English)")
- lines.append(
- "\u2502 \u2502 \u2514\u2500\u2500 ... # Other providers (Chinese & English versions)"
- )
- lines.append("\u2502 \u2502")
- lines.append("\u2502 \u251c\u2500\u2500 docker.md # Docker documentation (Chinese)")
- lines.append("\u2502 \u251c\u2500\u2500 docker.en.md # Docker documentation (English)")
- lines.append("\u2502 \u251c\u2500\u2500 install.md # Installation guide (Chinese)")
- lines.append("\u2502 \u251c\u2500\u2500 install.en.md # Installation guide (English)")
- lines.append("\u2502 \u2514\u2500\u2500 img/ # Images and diagrams")
- lines.append("\u2502")
- # docker section
- lines.append("\u251c\u2500\u2500 docker/ # Docker configuration")
- lines.append("\u2502 \u251c\u2500\u2500 Dockerfile # Main Dockerfile")
- lines.append("\u2502 \u251c\u2500\u2500 glibc.Dockerfile # glibc-based build")
- lines.append("\u2502 \u251c\u2500\u2500 musl.Dockerfile # musl-based build")
- lines.append("\u2502 \u2514\u2500\u2500 entrypoint.sh # Container entrypoint script")
- lines.append("\u2502")
- # schema section - dynamic generation
- lines.append("\u251c\u2500\u2500 schema/ # JSON schemas")
- schema_dir = os.path.join(repo_root, "schema")
- schema_files = get_sorted_files(schema_dir, [".json"], version_sort=True)
- for i, f in enumerate(schema_files):
- is_last = i == len(schema_files) - 1
- prefix = "\u2502 \u2514\u2500\u2500 " if is_last else "\u2502 \u251c\u2500\u2500 "
- version = f[:-5] # Remove .json
- if version == "v2":
- desc = "Legacy schema v2"
- elif version == "v2.8":
- desc = "Legacy schema v2.8"
- elif version == "v4.0":
- desc = "Previous schema v4.0"
- elif version == "v4.1":
- desc = "Latest schema v4.1"
- else:
- desc = "Schema " + version
- padded_name = f.ljust(24)
- lines.append(prefix + padded_name + "# " + desc)
- lines.append("\u2502")
- # Root files
- lines.append("\u251c\u2500\u2500 run.py # Direct run script")
- lines.append("\u251c\u2500\u2500 install.sh # One-click install script")
- lines.append("\u251c\u2500\u2500 pyproject.toml # Python project configuration")
- lines.append("\u251c\u2500\u2500 setup.cfg # Setup configuration")
- lines.append("\u251c\u2500\u2500 .gitignore # Git ignore rules")
- lines.append("\u251c\u2500\u2500 LICENSE # MIT License")
- lines.append("\u251c\u2500\u2500 README.md # Main README (Chinese)")
- lines.append("\u2514\u2500\u2500 README.en.md # Main README (English)")
- return "\n".join(lines)
- def main():
- # type: () -> None
- """Main function to update AGENTS.md directory structure."""
- # Determine repository root
- script_dir = os.path.dirname(os.path.abspath(__file__))
- repo_root = os.path.dirname(os.path.dirname(script_dir))
- agents_file = os.path.join(repo_root, "AGENTS.md")
- if not os.path.exists(agents_file):
- print("Error: AGENTS.md not found at " + agents_file)
- sys.exit(1)
- # Read current AGENTS.md
- with open(agents_file, "r", encoding="utf-8") as f:
- agents_content = f.read()
- # Extract current structure
- current_structure = extract_current_structure(agents_content)
- if current_structure is None:
- print("Error: Could not find directory structure section in AGENTS.md")
- sys.exit(1)
- # Generate new structure
- new_structure = generate_full_structure(repo_root)
- # Check if structure has changed
- if current_structure.strip() == new_structure.strip():
- print("No changes detected in directory structure.")
- sys.exit(0)
- # Update AGENTS.md content
- updated_content = update_agents_structure(agents_content, new_structure)
- # Write updated content
- with open(agents_file, "w", encoding="utf-8") as f:
- f.write(updated_content)
- print("AGENTS.md directory structure has been updated.")
- print("\nChanges detected:")
- print("Old structure lines: " + str(len(current_structure.strip().split("\n"))))
- print("New structure lines: " + str(len(new_structure.strip().split("\n"))))
- sys.exit(0)
- if __name__ == "__main__":
- main()
|