apipatch 0.3.0__py3-none-any.whl

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.

Potentially problematic release.


This version of apipatch might be problematic. Click here for more details.

apipatch/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """
2
+ ApiPatch - Autonomous AI Agent for API Breaking Changes & Self-Maintaining Codebases
3
+ Detects and fixes deprecated third-party API calls in Python, JavaScript, TypeScript,
4
+ JSX, TSX, and any other language — powered by LLM reasoning.
5
+ """
6
+
7
+ __version__ = "0.3.0"
8
+ __author__ = "Morad Moqbel"
9
+ __license__ = "MIT"
10
+
11
+ from apipatch.engine import ApiPatchEngine
12
+ from apipatch.validator import CodeValidator
13
+ from apipatch.auto_detector import AutoDeprecationDetector
14
+ from apipatch.proactive_hunter import GitHubPRHunter
15
+
16
+ __all__ = [
17
+ "ApiPatchEngine",
18
+ "CodeValidator",
19
+ "AutoDeprecationDetector",
20
+ "GitHubPRHunter",
21
+ "__version__",
22
+ ]
@@ -0,0 +1,223 @@
1
+ """
2
+ ApiPatch Autonomous Dependency & Import Detector
3
+ Discovers all third-party libraries used in a project (Python, JavaScript, TypeScript)
4
+ without any hardcoded rules — enabling the LLM engine to reason about ANY library.
5
+ """
6
+
7
+ import os
8
+ import ast
9
+ import json
10
+ import re
11
+ from typing import List, Set, Dict, Any, Optional
12
+ from apipatch.providers.factory import ProviderFactory
13
+
14
+ # Python standard library module names (excluded from "third-party" list)
15
+ STANDARD_LIB_MODULES = {
16
+ "abc", "argparse", "array", "ast", "asyncio", "base64", "binascii", "bisect",
17
+ "builtins", "calendar", "cmath", "collections", "concurrent", "contextlib",
18
+ "copy", "csv", "ctypes", "dataclasses", "datetime", "decimal", "difflib",
19
+ "dis", "doctest", "email", "enum", "errno", "faulthandler", "fcntl", "filecmp",
20
+ "fileinput", "fnmatch", "fractions", "functools", "gc", "getopt", "getpass",
21
+ "gettext", "glob", "graphlib", "gzip", "hashlib", "heapq", "hmac", "html",
22
+ "http", "idlelib", "imaplib", "imghdr", "importlib", "inspect", "io",
23
+ "ipaddress", "itertools", "json", "keyword", "linecache", "locale", "logging",
24
+ "lzma", "mailbox", "mailcap", "marshal", "math", "mimetypes", "mmap",
25
+ "modulefinder", "multiprocessing", "netrc", "nntplib", "numbers", "operator",
26
+ "os", "pathlib", "pdb", "pickle", "pipes", "pkgutil", "platform", "plistlib",
27
+ "poplib", "posix", "pprint", "profile", "pstats", "pty", "pwd", "py_compile",
28
+ "pyclbr", "pydoc", "queue", "quopri", "random", "re", "readline", "reprlib",
29
+ "resource", "rlcompleter", "runpy", "sched", "secrets", "select", "selectors",
30
+ "shelve", "shlex", "shutil", "signal", "site", "smtpd", "smtplib", "sndhdr",
31
+ "socket", "socketserver", "spwd", "sqlite3", "ssl", "stat", "statistics",
32
+ "string", "stringprep", "struct", "subprocess", "sunau", "symtable", "sys",
33
+ "sysconfig", "syslog", "tabnanny", "tarfile", "telnetlib", "tempfile", "termios",
34
+ "test", "textwrap", "threading", "time", "timeit", "tkinter", "token",
35
+ "tokenize", "tomllib", "trace", "traceback", "tracemalloc", "tty", "turtle",
36
+ "turtledemo", "types", "typing", "unicodedata", "unittest", "urllib", "uu",
37
+ "uuid", "venv", "warnings", "wave", "weakref", "webbrowser", "winreg", "winsound",
38
+ "wsgiref", "xdrlib", "xml", "xmlrpc", "zipapp", "zipfile", "zipimport", "zlib",
39
+ "zoneinfo"
40
+ }
41
+
42
+ # Node.js built-in modules (excluded from third-party list for JS/TS projects)
43
+ NODE_BUILTIN_MODULES = {
44
+ "fs", "path", "http", "https", "os", "crypto", "stream", "util", "events",
45
+ "child_process", "cluster", "dgram", "dns", "domain", "net", "querystring",
46
+ "readline", "repl", "string_decoder", "timers", "tls", "tty", "url",
47
+ "v8", "vm", "worker_threads", "zlib", "buffer", "assert", "console",
48
+ "module", "process", "inspector", "perf_hooks", "async_hooks"
49
+ }
50
+
51
+
52
+ class AutoDeprecationDetector:
53
+ def __init__(
54
+ self,
55
+ target_dir: str = ".",
56
+ provider_name: Optional[str] = None,
57
+ api_key: Optional[str] = None
58
+ ):
59
+ self.target_dir = os.path.abspath(target_dir)
60
+ self.provider = ProviderFactory.get_provider(
61
+ provider_name=provider_name, api_key=api_key
62
+ )
63
+
64
+ # ─── Python Import Extraction ─────────────────────────────────────────────
65
+
66
+ def extract_imports_from_file(self, file_path: str) -> Set[str]:
67
+ """
68
+ Uses Python AST to extract all third-party imported module names
69
+ from a .py or .pyw source file.
70
+ """
71
+ imports: Set[str] = set()
72
+ if not file_path.endswith((".py", ".pyw")):
73
+ return imports
74
+ try:
75
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
76
+ content = f.read()
77
+ tree = ast.parse(content)
78
+ for node in ast.walk(tree):
79
+ if isinstance(node, ast.Import):
80
+ for alias in node.names:
81
+ top_pkg = alias.name.split('.')[0]
82
+ if top_pkg not in STANDARD_LIB_MODULES:
83
+ imports.add(top_pkg)
84
+ elif isinstance(node, ast.ImportFrom):
85
+ if node.module:
86
+ top_pkg = node.module.split('.')[0]
87
+ if top_pkg not in STANDARD_LIB_MODULES:
88
+ imports.add(top_pkg)
89
+ except Exception:
90
+ pass
91
+ return imports
92
+
93
+ # ─── JS/TS Import Extraction ──────────────────────────────────────────────
94
+
95
+ def extract_imports_from_js_file(self, file_path: str) -> Set[str]:
96
+ """
97
+ Uses regex to extract third-party package names from
98
+ JS / TS / JSX / TSX / MJS / CJS source files.
99
+ """
100
+ imports: Set[str] = set()
101
+ ext = os.path.splitext(file_path)[1].lower()
102
+ if ext not in {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}:
103
+ return imports
104
+ try:
105
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
106
+ content = f.read()
107
+
108
+ # ESM: import ... from 'pkg' / import 'pkg'
109
+ esm_patterns = re.findall(
110
+ r"""(?:^|\n)\s*import\s+(?:[^'"]*?from\s+)?['"]([@\w][\w/.-]*)['"]""",
111
+ content
112
+ )
113
+ # CJS: require('pkg')
114
+ cjs_patterns = re.findall(r"""require\s*\(\s*['"]([@\w][\w/.-]*)['"]\s*\)""", content)
115
+
116
+ for raw in esm_patterns + cjs_patterns:
117
+ # Normalise scoped packages: @org/pkg -> @org/pkg
118
+ # Strip sub-paths: lodash/merge -> lodash
119
+ if raw.startswith('@'):
120
+ parts = raw.split('/')
121
+ pkg = '/'.join(parts[:2]) if len(parts) >= 2 else raw
122
+ else:
123
+ pkg = raw.split('/')[0]
124
+
125
+ if pkg and pkg not in NODE_BUILTIN_MODULES and not pkg.startswith('.'):
126
+ imports.add(pkg)
127
+ except Exception:
128
+ pass
129
+ return imports
130
+
131
+ # ─── Manifest-based Dependency Discovery ─────────────────────────────────
132
+
133
+ def _parse_requirements_txt(self) -> Set[str]:
134
+ deps: Set[str] = set()
135
+ req_path = os.path.join(self.target_dir, "requirements.txt")
136
+ if os.path.exists(req_path):
137
+ with open(req_path, "r", encoding="utf-8", errors="ignore") as f:
138
+ for line in f:
139
+ line = line.strip()
140
+ if line and not line.startswith("#") and not line.startswith("-"):
141
+ pkg = re.split(r"[><=~;!@\[]", line)[0].strip().lower()
142
+ if pkg:
143
+ deps.add(pkg)
144
+ return deps
145
+
146
+ def _parse_package_json(self) -> Set[str]:
147
+ deps: Set[str] = set()
148
+ pkg_json = os.path.join(self.target_dir, "package.json")
149
+ if os.path.exists(pkg_json):
150
+ try:
151
+ with open(pkg_json, "r", encoding="utf-8", errors="ignore") as f:
152
+ data = json.load(f)
153
+ for key in ("dependencies", "devDependencies", "peerDependencies"):
154
+ if key in data and isinstance(data[key], dict):
155
+ deps.update(p.lower() for p in data[key].keys())
156
+ except Exception:
157
+ pass
158
+ return deps
159
+
160
+ def _parse_pyproject_toml(self) -> Set[str]:
161
+ deps: Set[str] = set()
162
+ pyproject = os.path.join(self.target_dir, "pyproject.toml")
163
+ if os.path.exists(pyproject):
164
+ try:
165
+ with open(pyproject, "r", encoding="utf-8", errors="ignore") as f:
166
+ content = f.read()
167
+ # Extract quoted package names from dependency lists
168
+ for match in re.findall(r'"([a-zA-Z0-9_\-]+)(?:[><=~;].*)?[">\s]', content):
169
+ m = match.strip().lower()
170
+ if m and m not in {"apipatch", "setuptools", "wheel", "pytest", "python"}:
171
+ deps.add(m)
172
+ except Exception:
173
+ pass
174
+ return deps
175
+
176
+ # ─── Main Detection API ───────────────────────────────────────────────────
177
+
178
+ def detect_dependencies(self) -> List[str]:
179
+ """
180
+ Discovers all third-party packages from:
181
+ 1. requirements.txt
182
+ 2. package.json
183
+ 3. pyproject.toml
184
+ 4. Recursive AST inspection of Python source files
185
+ 5. Regex inspection of JS/TS source files
186
+ """
187
+ dependencies: Set[str] = set()
188
+
189
+ dependencies.update(self._parse_requirements_txt())
190
+ dependencies.update(self._parse_package_json())
191
+ dependencies.update(self._parse_pyproject_toml())
192
+
193
+ ignore_dirs = {
194
+ ".git", "node_modules", "venv", ".venv", "__pycache__",
195
+ ".gemini", "dist", "build", ".next", ".nuxt", "out", "coverage"
196
+ }
197
+
198
+ for root, dirs, files in os.walk(self.target_dir):
199
+ dirs[:] = [d for d in dirs if d not in ignore_dirs]
200
+ for file in files:
201
+ full_path = os.path.join(root, file)
202
+ ext = os.path.splitext(file)[1].lower()
203
+ if ext in {".py", ".pyw"}:
204
+ dependencies.update(self.extract_imports_from_file(full_path))
205
+ elif ext in {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}:
206
+ dependencies.update(self.extract_imports_from_js_file(full_path))
207
+
208
+ return sorted(dependencies)
209
+
210
+ def run_autonomous_discovery(self) -> Dict[str, Any]:
211
+ """Runs end-to-end autonomous dependency discovery and prints a summary."""
212
+ deps = self.detect_dependencies()
213
+ print(f"\n[ApiPatch] Detected {len(deps)} third-party package(s) in '{self.target_dir}':")
214
+ for dep in deps:
215
+ print(f" - {dep}")
216
+ print(
217
+ "\nAll detected packages will be passed to the AI engine "
218
+ "for dynamic deprecation analysis when you run `apipatch scan` or `apipatch fix`.\n"
219
+ )
220
+ return {
221
+ "target_directory": self.target_dir,
222
+ "detected_packages": deps,
223
+ }
apipatch/cli.py ADDED
@@ -0,0 +1,101 @@
1
+ """
2
+ ApiPatch Command Line Interface (CLI)
3
+ Provides modern terminal commands for autonomous API code audits and automated refactoring.
4
+ """
5
+
6
+ import sys
7
+ import argparse
8
+ from typing import Optional
9
+ from apipatch import __version__
10
+ from apipatch.engine import ApiPatchEngine, Colors
11
+ from apipatch.auto_detector import AutoDeprecationDetector
12
+ from apipatch.proactive_hunter import GitHubPRHunter
13
+
14
+ # Ensure UTF-8 console output on Windows
15
+ if sys.stdout and hasattr(sys.stdout, "reconfigure"):
16
+ sys.stdout.reconfigure(encoding="utf-8")
17
+
18
+
19
+ def main():
20
+ parser = argparse.ArgumentParser(
21
+ prog="apipatch",
22
+ description=f"{Colors.HEADER}⚡ ApiPatch v{__version__} - Autonomous AI Agent for API Breaking Changes{Colors.ENDC}",
23
+ formatter_class=argparse.RawDescriptionHelpFormatter
24
+ )
25
+ parser.add_argument("-v", "--version", action="version", version=f"apipatch {__version__}")
26
+
27
+ subparsers = parser.add_subparsers(dest="command", help="Available subcommands")
28
+
29
+ # Command: scan
30
+ scan_parser = subparsers.add_parser("scan", help="Scan a codebase for deprecated API signatures")
31
+ scan_parser.add_argument("path", nargs="?", default=".", help="File or directory path to scan (default: current dir)")
32
+ scan_parser.add_argument("--provider", choices=["openai", "anthropic", "gemini"], help="AI provider for dynamic reasoning")
33
+ scan_parser.add_argument("--api-key", help="API key for chosen provider")
34
+ scan_parser.add_argument("--model", help="Specific model name (e.g., gpt-4o, claude-3-7-sonnet)")
35
+
36
+ # Command: fix
37
+ fix_parser = subparsers.add_parser("fix", help="Audit and generate modernized code refactors")
38
+ fix_parser.add_argument("path", nargs="?", default=".", help="File or directory path to refactor")
39
+ fix_parser.add_argument("-w", "--write", action="store_true", help="Apply fixes in-place to files on disk")
40
+ fix_parser.add_argument("--no-backup", action="store_true", help="Disable automatic .bak file creation when writing")
41
+ fix_parser.add_argument("--provider", choices=["openai", "anthropic", "gemini"], help="AI provider for dynamic reasoning")
42
+ fix_parser.add_argument("--api-key", help="API key for chosen provider")
43
+ fix_parser.add_argument("--model", help="Specific model name")
44
+
45
+ # Command: detect
46
+ detect_parser = subparsers.add_parser("detect", help="Auto-discover project dependencies and deprecation rules")
47
+ detect_parser.add_argument("path", nargs="?", default=".", help="Project directory path (default: current dir)")
48
+
49
+ # Command: hunt
50
+ hunt_parser = subparsers.add_parser("hunt", help="Proactively search GitHub for deprecated code and prepare PRs")
51
+ hunt_parser.add_argument("query", nargs="?", default="openai.ChatCompletion.create language:python", help="GitHub Code Search query")
52
+ hunt_parser.add_argument("--max", type=int, default=3, help="Max candidate repositories to inspect")
53
+ hunt_parser.add_argument("--token", help="GitHub Personal Access Token")
54
+
55
+ args = parser.parse_args()
56
+
57
+ if not args.command:
58
+ # Default behavior if no subcommand passed: interactive help or scan
59
+ parser.print_help()
60
+ sys.exit(0)
61
+
62
+ if args.command == "scan":
63
+ engine = ApiPatchEngine(
64
+ provider_name=args.provider,
65
+ api_key=args.api_key,
66
+ model=args.model,
67
+ create_backup=False
68
+ )
69
+ if os_is_file(args.path):
70
+ engine.process_file(args.path, write_in_place=False)
71
+ else:
72
+ engine.process_directory(args.path, write_in_place=False)
73
+
74
+ elif args.command == "fix":
75
+ engine = ApiPatchEngine(
76
+ provider_name=args.provider,
77
+ api_key=args.api_key,
78
+ model=args.model,
79
+ create_backup=not args.no_backup
80
+ )
81
+ if os_is_file(args.path):
82
+ engine.process_file(args.path, write_in_place=args.write)
83
+ else:
84
+ engine.process_directory(args.path, write_in_place=args.write)
85
+
86
+ elif args.command == "detect":
87
+ detector = AutoDeprecationDetector(target_dir=args.path)
88
+ detector.run_autonomous_discovery()
89
+
90
+ elif args.command == "hunt":
91
+ hunter = GitHubPRHunter(github_token=args.token)
92
+ hunter.hunt_and_preview(query=args.query, max_results=args.max)
93
+
94
+
95
+ def os_is_file(path: str) -> bool:
96
+ import os
97
+ return os.path.isfile(path)
98
+
99
+
100
+ if __name__ == "__main__":
101
+ main()
apipatch/engine.py ADDED
@@ -0,0 +1,260 @@
1
+ """
2
+ ApiPatch Core Orchestration Engine
3
+ Pure LLM-powered autonomous agent for detecting and fixing deprecated API calls
4
+ across ANY third-party library in Python, JavaScript, TypeScript, JSX, TSX, and more.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import shutil
10
+ import difflib
11
+ from typing import Dict, Any, List, Optional, Set
12
+ from apipatch.validator import CodeValidator, ValidationResult
13
+ from apipatch.providers.factory import ProviderFactory
14
+ from apipatch.auto_detector import AutoDeprecationDetector
15
+
16
+ # Reconfigure stdout/stderr for safe Unicode / Windows encoding
17
+ if sys.stdout and hasattr(sys.stdout, "reconfigure"):
18
+ try:
19
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
20
+ except Exception:
21
+ pass
22
+
23
+ if sys.stderr and hasattr(sys.stderr, "reconfigure"):
24
+ try:
25
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
26
+ except Exception:
27
+ pass
28
+
29
+
30
+ class Colors:
31
+ HEADER = '\033[95m'
32
+ OKBLUE = '\033[94m'
33
+ OKCYAN = '\033[96m'
34
+ OKGREEN = '\033[92m'
35
+ WARNING = '\033[93m'
36
+ FAIL = '\033[91m'
37
+ ENDC = '\033[0m'
38
+ BOLD = '\033[1m'
39
+
40
+
41
+ def safe_print(text: str):
42
+ """Safely prints to stdout without dying on encoding mismatches."""
43
+ try:
44
+ print(text)
45
+ except UnicodeEncodeError:
46
+ try:
47
+ print(text.encode("ascii", errors="replace").decode("ascii"))
48
+ except Exception:
49
+ pass
50
+
51
+
52
+ class ApiPatchEngine:
53
+ def __init__(
54
+ self,
55
+ provider_name: Optional[str] = None,
56
+ api_key: Optional[str] = None,
57
+ model: Optional[str] = None,
58
+ create_backup: bool = True
59
+ ):
60
+ self.provider = ProviderFactory.get_provider(
61
+ provider_name=provider_name,
62
+ api_key=api_key,
63
+ model=model
64
+ )
65
+ self.create_backup = create_backup
66
+ self.detector = AutoDeprecationDetector()
67
+
68
+ def audit_code(
69
+ self,
70
+ file_path: str,
71
+ code: str,
72
+ detected_libraries: Optional[List[str]] = None
73
+ ) -> Dict[str, Any]:
74
+ """
75
+ Fully LLM-driven audit:
76
+ Detects and fixes deprecated/breaking API calls across ANY library,
77
+ in Python, JavaScript, TypeScript, JSX, TSX, etc.
78
+ Falls back gracefully if no provider is configured.
79
+ """
80
+ file_name = os.path.basename(file_path)
81
+ _, ext = os.path.splitext(file_path)
82
+
83
+ empty_result = {
84
+ "has_breaking_changes": False,
85
+ "detected_issues": [],
86
+ "refactored_code": code
87
+ }
88
+
89
+ if not self.provider:
90
+ safe_print(
91
+ f" {Colors.WARNING}[!] No AI provider configured. "
92
+ f"Set GEMINI_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY "
93
+ f"to enable full analysis.{Colors.ENDC}"
94
+ )
95
+ return empty_result
96
+
97
+ try:
98
+ libs = detected_libraries or list(
99
+ self.detector.extract_imports_from_file(file_path)
100
+ )
101
+ llm_res = self.provider.audit_code(file_name, code, detected_libraries=libs)
102
+
103
+ if llm_res.get("has_breaking_changes") and llm_res.get("refactored_code"):
104
+ refactored = llm_res["refactored_code"]
105
+ val = CodeValidator.validate(code, refactored, file_extension=ext)
106
+ if val.is_valid:
107
+ return llm_res
108
+ else:
109
+ safe_print(
110
+ f" {Colors.WARNING}[!] LLM output failed safety validation "
111
+ f"({val.error_message}). Keeping original.{Colors.ENDC}"
112
+ )
113
+ return empty_result
114
+
115
+ return llm_res if llm_res else empty_result
116
+
117
+ except Exception as e:
118
+ safe_print(f" {Colors.WARNING}[!] Provider error: {e}{Colors.ENDC}")
119
+ return empty_result
120
+
121
+ def generate_diff(self, old_code: str, new_code: str, file_name: str) -> List[str]:
122
+ """Generates unified diff lines between old and new code."""
123
+ old_lines = old_code.splitlines(keepends=True)
124
+ new_lines = new_code.splitlines(keepends=True)
125
+ return list(difflib.unified_diff(
126
+ old_lines,
127
+ new_lines,
128
+ fromfile=f"a/{file_name}",
129
+ tofile=f"b/{file_name}"
130
+ ))
131
+
132
+ def print_diff(self, diff: List[str]):
133
+ """Prints colorized unified diff to stdout."""
134
+ if not diff:
135
+ safe_print(" (No changes needed)")
136
+ return
137
+
138
+ for line in diff:
139
+ if line.startswith('+') and not line.startswith('+++'):
140
+ safe_print(f"{Colors.OKGREEN}{line.rstrip()}{Colors.ENDC}")
141
+ elif line.startswith('-') and not line.startswith('---'):
142
+ safe_print(f"{Colors.FAIL}{line.rstrip()}{Colors.ENDC}")
143
+ elif line.startswith('@'):
144
+ safe_print(f"{Colors.OKCYAN}{line.rstrip()}{Colors.ENDC}")
145
+ else:
146
+ safe_print(f" {line.rstrip()}")
147
+
148
+ def process_file(
149
+ self,
150
+ file_path: str,
151
+ write_in_place: bool = False,
152
+ detected_libraries: Optional[List[str]] = None
153
+ ) -> Dict[str, Any]:
154
+ """Audits, refactors, previews diff, and optionally writes fixed code to disk."""
155
+ file_path = os.path.abspath(file_path)
156
+ try:
157
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
158
+ original_code = f.read()
159
+ except Exception as e:
160
+ safe_print(f"{Colors.FAIL}[!] Cannot read {file_path}: {e}{Colors.ENDC}")
161
+ return {"file": file_path, "status": "error", "issues": []}
162
+
163
+ audit = self.audit_code(file_path, original_code, detected_libraries=detected_libraries)
164
+ file_name = os.path.basename(file_path)
165
+
166
+ if not audit["has_breaking_changes"]:
167
+ safe_print(f"{Colors.OKGREEN}[✓] {file_name}: Clean — no breaking changes detected.{Colors.ENDC}")
168
+ return {"file": file_path, "status": "clean", "issues": []}
169
+
170
+ issues = audit["detected_issues"]
171
+ safe_print(f"\n{Colors.FAIL}[!] [{len(issues)} DEPRECATION(S)] {file_path}{Colors.ENDC}")
172
+ for idx, issue in enumerate(issues, 1):
173
+ safe_print(f" {Colors.BOLD}#{idx} [{issue['library']}]:{Colors.ENDC} {issue['description']}")
174
+ safe_print(f" Deprecated : {Colors.FAIL}{issue['deprecated_symbol']}{Colors.ENDC}")
175
+ safe_print(f" Replacement: {Colors.OKGREEN}{issue['replacement_symbol']}{Colors.ENDC}")
176
+
177
+ refactored = audit.get("refactored_code", original_code)
178
+ diff = self.generate_diff(original_code, refactored, file_name)
179
+
180
+ safe_print(f"\n{Colors.OKBLUE}>> Refactored Diff Preview:{Colors.ENDC}")
181
+ self.print_diff(diff)
182
+
183
+ if write_in_place and refactored != original_code:
184
+ if self.create_backup:
185
+ backup_path = f"{file_path}.bak"
186
+ shutil.copy2(file_path, backup_path)
187
+ safe_print(f" {Colors.OKCYAN}[*] Backup saved → {backup_path}{Colors.ENDC}")
188
+
189
+ with open(file_path, "w", encoding="utf-8") as f:
190
+ f.write(refactored)
191
+ safe_print(f" {Colors.OKGREEN}[✓] Updated {file_name} in place.{Colors.ENDC}")
192
+
193
+ first_lib = issues[0]['library'] if issues else "API"
194
+ safe_print(
195
+ f"\n{Colors.HEADER}[PR Ready]:{Colors.ENDC} "
196
+ f"[ApiPatch] Migrate deprecated {first_lib} calls in {file_name}"
197
+ )
198
+
199
+ return {
200
+ "file": file_path,
201
+ "status": "refactored" if write_in_place else "detected",
202
+ "issues": issues,
203
+ "refactored_code": refactored
204
+ }
205
+
206
+ def process_directory(
207
+ self,
208
+ target_dir: str,
209
+ write_in_place: bool = False
210
+ ) -> Dict[str, Any]:
211
+ """Recursively audits all supported source files in target directory."""
212
+ target_dir = os.path.abspath(target_dir)
213
+ safe_print(f"{Colors.HEADER}{Colors.BOLD}=== ApiPatch: Autonomous AI Codebase Auditor ==={Colors.ENDC}")
214
+ safe_print(f"Target Directory : {Colors.OKCYAN}{target_dir}{Colors.ENDC}")
215
+ provider_name = self.provider.__class__.__name__ if self.provider else "No Provider (Offline)"
216
+ safe_print(f"Active AI Engine : {Colors.OKGREEN}{provider_name}{Colors.ENDC}\n")
217
+
218
+ # Discover project-wide dependencies
219
+ detector = AutoDeprecationDetector(target_dir)
220
+ deps = detector.detect_dependencies()
221
+ if deps:
222
+ listed = ', '.join(deps[:12])
223
+ extra = f'... (+{len(deps) - 12} more)' if len(deps) > 12 else ''
224
+ safe_print(f"[*] Detected {len(deps)} dependencies: {listed}{extra}\n")
225
+
226
+ total_scanned = 0
227
+ affected_files = 0
228
+ results = []
229
+
230
+ ignore_dirs = {
231
+ ".git", "node_modules", "venv", ".venv", "__pycache__",
232
+ ".gemini", "dist", "build", ".next", ".nuxt", "out", "coverage"
233
+ }
234
+ supported_exts = {".py", ".pyw", ".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}
235
+
236
+ for root, dirs, files in os.walk(target_dir):
237
+ dirs[:] = [d for d in dirs if d not in ignore_dirs]
238
+ for file in files:
239
+ _, ext = os.path.splitext(file)
240
+ if ext in supported_exts:
241
+ total_scanned += 1
242
+ full_path = os.path.join(root, file)
243
+ res = self.process_file(
244
+ full_path,
245
+ write_in_place=write_in_place,
246
+ detected_libraries=deps
247
+ )
248
+ if res["status"] in {"detected", "refactored"}:
249
+ affected_files += 1
250
+ results.append(res)
251
+
252
+ safe_print(
253
+ f"\n[Audit Summary]: {total_scanned} file(s) inspected, "
254
+ f"{affected_files} file(s) with deprecated APIs found."
255
+ )
256
+ return {
257
+ "total_scanned": total_scanned,
258
+ "affected_files": affected_files,
259
+ "results": results
260
+ }