auto-code-fixer 0.3.4__tar.gz → 0.3.6__tar.gz

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.
Files changed (33) hide show
  1. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/PKG-INFO +16 -1
  2. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/README.md +15 -0
  3. auto_code_fixer-0.3.6/auto_code_fixer/__init__.py +1 -0
  4. auto_code_fixer-0.3.6/auto_code_fixer/cli.py +554 -0
  5. auto_code_fixer-0.3.6/auto_code_fixer/fixer.py +156 -0
  6. auto_code_fixer-0.3.6/auto_code_fixer/patch_protocol.py +102 -0
  7. auto_code_fixer-0.3.6/auto_code_fixer/patcher.py +67 -0
  8. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/auto_code_fixer.egg-info/PKG-INFO +16 -1
  9. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/auto_code_fixer.egg-info/SOURCES.txt +4 -1
  10. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/pyproject.toml +1 -1
  11. auto_code_fixer-0.3.6/tests/test_atomic_write.py +34 -0
  12. auto_code_fixer-0.3.6/tests/test_patch_protocol.py +46 -0
  13. auto_code_fixer-0.3.4/auto_code_fixer/__init__.py +0 -1
  14. auto_code_fixer-0.3.4/auto_code_fixer/cli.py +0 -311
  15. auto_code_fixer-0.3.4/auto_code_fixer/fixer.py +0 -79
  16. auto_code_fixer-0.3.4/auto_code_fixer/patcher.py +0 -31
  17. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/LICENSE +0 -0
  18. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/auto_code_fixer/command_runner.py +0 -0
  19. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/auto_code_fixer/installer.py +0 -0
  20. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/auto_code_fixer/models.py +0 -0
  21. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/auto_code_fixer/plan.py +0 -0
  22. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/auto_code_fixer/runner.py +0 -0
  23. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/auto_code_fixer/sandbox.py +0 -0
  24. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/auto_code_fixer/traceback_utils.py +0 -0
  25. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/auto_code_fixer/utils.py +0 -0
  26. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/auto_code_fixer/venv_manager.py +0 -0
  27. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/auto_code_fixer.egg-info/dependency_links.txt +0 -0
  28. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/auto_code_fixer.egg-info/entry_points.txt +0 -0
  29. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/auto_code_fixer.egg-info/requires.txt +0 -0
  30. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/auto_code_fixer.egg-info/top_level.txt +0 -0
  31. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/setup.cfg +0 -0
  32. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/tests/test_fix_imported_file.py +0 -0
  33. {auto_code_fixer-0.3.4 → auto_code_fixer-0.3.6}/tests/test_internal_imports.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: auto-code-fixer
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: Automatically fix Python code using ChatGPT
5
5
  Author-email: Arif Shah <ashah7775@gmail.com>
6
6
  License: MIT
@@ -121,6 +121,21 @@ auto-code-fixer main.py --ai-plan
121
121
  ```
122
122
  This enables a helper that can suggest which local file to edit. It is best-effort.
123
123
 
124
+ ### Optional structured patch protocol (JSON + sha256)
125
+ ```bash
126
+ auto-code-fixer main.py --patch-protocol
127
+ ```
128
+ When enabled, the model is asked to return strict JSON with `{files:[{path,new_content,sha256}]}`.
129
+ The tool verifies the SHA-256 hash of `new_content` before applying edits, and falls back to the
130
+ legacy full-text mode if parsing/validation fails.
131
+
132
+ ### Optional formatting / linting (best-effort)
133
+ ```bash
134
+ auto-code-fixer main.py --format black
135
+ auto-code-fixer main.py --lint ruff --fix
136
+ ```
137
+ These run inside the sandbox venv and are skipped if the tools are not installed.
138
+
124
139
  ---
125
140
 
126
141
  ## Environment variables
@@ -100,6 +100,21 @@ auto-code-fixer main.py --ai-plan
100
100
  ```
101
101
  This enables a helper that can suggest which local file to edit. It is best-effort.
102
102
 
103
+ ### Optional structured patch protocol (JSON + sha256)
104
+ ```bash
105
+ auto-code-fixer main.py --patch-protocol
106
+ ```
107
+ When enabled, the model is asked to return strict JSON with `{files:[{path,new_content,sha256}]}`.
108
+ The tool verifies the SHA-256 hash of `new_content` before applying edits, and falls back to the
109
+ legacy full-text mode if parsing/validation fails.
110
+
111
+ ### Optional formatting / linting (best-effort)
112
+ ```bash
113
+ auto-code-fixer main.py --format black
114
+ auto-code-fixer main.py --lint ruff --fix
115
+ ```
116
+ These run inside the sandbox venv and are skipped if the tools are not installed.
117
+
103
118
  ---
104
119
 
105
120
  ## Environment variables
@@ -0,0 +1 @@
1
+ __version__ = "0.3.6"
@@ -0,0 +1,554 @@
1
+ import argparse
2
+ import os
3
+ import shutil
4
+ import tempfile
5
+ import time
6
+ from decouple import config
7
+
8
+ from auto_code_fixer.runner import run_code
9
+ from auto_code_fixer.fixer import fix_code_with_gpt
10
+ from auto_code_fixer.installer import check_and_install_missing_lib
11
+ from auto_code_fixer.utils import (
12
+ discover_all_files,
13
+ is_in_project,
14
+ log,
15
+ set_verbose,
16
+ )
17
+ from auto_code_fixer import __version__
18
+
19
+ DEFAULT_MAX_RETRIES = 8 # can be overridden via CLI
20
+
21
+
22
+ def fix_file(
23
+ file_path,
24
+ project_root,
25
+ api_key,
26
+ ask,
27
+ verbose,
28
+ *,
29
+ dry_run: bool,
30
+ model: str | None,
31
+ timeout_s: int,
32
+ max_retries: int,
33
+ run_cmd: str | None,
34
+ patch_protocol: bool,
35
+ max_files_changed: int,
36
+ context_files: int,
37
+ approve: bool,
38
+ fmt: str | None,
39
+ lint: str | None,
40
+ lint_fix: bool,
41
+ ) -> bool:
42
+ log(f"Processing entry file: {file_path}")
43
+
44
+ project_root = os.path.abspath(project_root)
45
+ file_path = os.path.abspath(file_path)
46
+
47
+ # Build sandbox with entry + imported local files
48
+ from auto_code_fixer.sandbox import make_sandbox
49
+ sandbox_root, sandbox_entry = make_sandbox(entry_file=file_path, project_root=project_root)
50
+
51
+ # Always delete temp sandbox (best-effort). Also register atexit cleanup in case of crashes.
52
+ import atexit
53
+
54
+ def cleanup_sandbox() -> None:
55
+ try:
56
+ shutil.rmtree(sandbox_root)
57
+ except FileNotFoundError:
58
+ return
59
+ except Exception as e:
60
+ log(f"WARN: failed to delete sandbox dir {sandbox_root}: {e}")
61
+
62
+ atexit.register(cleanup_sandbox)
63
+
64
+ # Create isolated venv in sandbox
65
+ from auto_code_fixer.venv_manager import create_venv
66
+ from auto_code_fixer.patcher import backup_file
67
+
68
+ venv_python = create_venv(sandbox_root)
69
+
70
+ def _run_optional_formatters_and_linters() -> None:
71
+ # Best-effort formatting/linting (only if tools are installed in the sandbox venv).
72
+ from auto_code_fixer.command_runner import run_command
73
+
74
+ if fmt == "black":
75
+ rc, out, err = run_command(
76
+ "python -m black .",
77
+ timeout_s=max(timeout_s, 60),
78
+ python_exe=venv_python,
79
+ cwd=sandbox_root,
80
+ extra_env={"PYTHONPATH": sandbox_root},
81
+ )
82
+ if rc == 0:
83
+ log("Formatted with black", "DEBUG")
84
+ else:
85
+ # If black isn't installed, ignore.
86
+ if "No module named" in (err or "") and "black" in (err or ""):
87
+ log("black not installed in sandbox venv; skipping format", "DEBUG")
88
+ else:
89
+ log(f"black failed (rc={rc}): {err}", "DEBUG")
90
+
91
+ if lint == "ruff":
92
+ cmd = "python -m ruff check ."
93
+ if lint_fix:
94
+ cmd += " --fix"
95
+ rc, out, err = run_command(
96
+ cmd,
97
+ timeout_s=max(timeout_s, 60),
98
+ python_exe=venv_python,
99
+ cwd=sandbox_root,
100
+ extra_env={"PYTHONPATH": sandbox_root},
101
+ )
102
+ if rc == 0:
103
+ log("ruff check passed", "DEBUG")
104
+ else:
105
+ if "No module named" in (err or "") and "ruff" in (err or ""):
106
+ log("ruff not installed in sandbox venv; skipping lint", "DEBUG")
107
+ else:
108
+ log(f"ruff reported issues (rc={rc}): {err}", "DEBUG")
109
+
110
+ changed_sandbox_files: set[str] = set()
111
+
112
+ for attempt in range(max_retries):
113
+ log(f"Run attempt #{attempt + 1}")
114
+
115
+ # Ensure local modules resolve inside sandbox
116
+ if run_cmd:
117
+ from auto_code_fixer.command_runner import run_command
118
+
119
+ retcode, stdout, stderr = run_command(
120
+ run_cmd,
121
+ timeout_s=timeout_s,
122
+ python_exe=venv_python,
123
+ cwd=sandbox_root,
124
+ extra_env={"PYTHONPATH": sandbox_root},
125
+ )
126
+ else:
127
+ retcode, stdout, stderr = run_code(
128
+ sandbox_entry,
129
+ timeout_s=timeout_s,
130
+ python_exe=venv_python,
131
+ cwd=sandbox_root,
132
+ extra_env={"PYTHONPATH": sandbox_root},
133
+ )
134
+
135
+ if verbose:
136
+ if stdout:
137
+ log(f"STDOUT:\n{stdout}", "DEBUG")
138
+ if stderr:
139
+ log(f"STDERR:\n{stderr}", "DEBUG")
140
+
141
+ if retcode == 0:
142
+ log("Script executed successfully ✅")
143
+
144
+ # Apply sandbox changes back to project (only if we actually changed something)
145
+ if attempt > 0 and is_in_project(file_path, project_root) and changed_sandbox_files:
146
+ rel_changes = [os.path.relpath(p, sandbox_root) for p in sorted(changed_sandbox_files)]
147
+
148
+ if ask:
149
+ confirm = input(
150
+ "Overwrite original files with fixed versions?\n"
151
+ + "\n".join(f"- {c}" for c in rel_changes)
152
+ + "\n(y/n): "
153
+ ).strip().lower()
154
+
155
+ if confirm != "y":
156
+ log("User declined overwrite", "WARN")
157
+ cleanup_sandbox()
158
+ return False
159
+
160
+ if dry_run:
161
+ log("DRY RUN: would apply fixes:\n" + "\n".join(rel_changes), "WARN")
162
+ else:
163
+ sr = os.path.realpath(os.path.abspath(sandbox_root))
164
+ pr = os.path.realpath(os.path.abspath(project_root))
165
+
166
+ for p in sorted(changed_sandbox_files):
167
+ p_real = os.path.realpath(os.path.abspath(p))
168
+
169
+ # compute rel using real paths to avoid macOS /private path weirdness
170
+ rel = os.path.relpath(p_real, sr)
171
+
172
+ # Safety: never allow paths escaping the sandbox
173
+ if rel.startswith(".." + os.sep) or rel == "..":
174
+ log(f"Skipping suspicious path outside sandbox: {p}", "WARN")
175
+ continue
176
+
177
+ dst = os.path.join(pr, rel)
178
+ dst_real = os.path.realpath(os.path.abspath(dst))
179
+
180
+ # Safety: never write outside the project root
181
+ if not (dst_real.startswith(pr + os.sep) or dst_real == pr):
182
+ log(f"Skipping suspicious destination outside project: {dst}", "WARN")
183
+ continue
184
+
185
+ # Avoid shutil.SameFileError
186
+ try:
187
+ if os.path.exists(dst_real) and os.path.samefile(p_real, dst_real):
188
+ log(f"Skip copy (same file): {dst_real}", "DEBUG")
189
+ continue
190
+ except Exception:
191
+ pass
192
+
193
+ if os.path.exists(dst_real):
194
+ bak = backup_file(dst_real)
195
+ log(f"Backup created: {bak}", "DEBUG")
196
+
197
+ os.makedirs(os.path.dirname(dst_real), exist_ok=True)
198
+ shutil.copy(p_real, dst_real)
199
+ log(f"File updated: {dst_real}")
200
+
201
+ cleanup_sandbox()
202
+ log(f"Fix completed in {attempt + 1} attempt(s) 🎉")
203
+ return True
204
+
205
+ log("Error detected ❌", "ERROR")
206
+ print(stderr)
207
+
208
+ if check_and_install_missing_lib(stderr, python_exe=venv_python, project_root=sandbox_root):
209
+ log("Missing dependency installed (venv), retrying…")
210
+ time.sleep(1)
211
+ continue
212
+
213
+ # Pick the most relevant local file from the traceback (entry or imported file)
214
+ from auto_code_fixer.traceback_utils import pick_relevant_file
215
+
216
+ target_file = pick_relevant_file(stderr, sandbox_root=sandbox_root) or sandbox_entry
217
+
218
+ # Safety: ensure target_file is within sandbox
219
+ try:
220
+ sr = os.path.realpath(os.path.abspath(sandbox_root))
221
+ tf = os.path.realpath(os.path.abspath(target_file))
222
+ if not (tf.startswith(sr + os.sep) or tf == sr):
223
+ target_file = sandbox_entry
224
+ except Exception:
225
+ target_file = sandbox_entry
226
+
227
+ # Optional AI fix plan can override file selection
228
+ try:
229
+ from auto_code_fixer.plan import ask_ai_for_fix_plan
230
+
231
+ plan = ask_ai_for_fix_plan(
232
+ sandbox_root=sandbox_root,
233
+ stderr=stderr,
234
+ api_key=api_key,
235
+ model=model,
236
+ )
237
+ if plan and plan.target_files:
238
+ # Use the first suggested file that exists
239
+ for rel in plan.target_files:
240
+ cand = os.path.abspath(os.path.join(sandbox_root, rel))
241
+ if cand.startswith(os.path.abspath(sandbox_root)) and os.path.exists(cand):
242
+ target_file = cand
243
+ break
244
+ except Exception:
245
+ pass
246
+
247
+ log(f"Sending {os.path.relpath(target_file, sandbox_root)} + error to GPT 🧠", "DEBUG")
248
+
249
+ applied_any = False
250
+
251
+ if patch_protocol:
252
+ try:
253
+ import difflib
254
+
255
+ from auto_code_fixer.fixer import fix_code_with_gpt_patch_protocol
256
+ from auto_code_fixer.patch_protocol import (
257
+ parse_patch_protocol_response,
258
+ validate_and_resolve_patch_files,
259
+ )
260
+ from auto_code_fixer.patcher import safe_read, atomic_write_verified_sha256
261
+ from auto_code_fixer.utils import find_imports
262
+
263
+ hint_paths = [os.path.relpath(target_file, sandbox_root)]
264
+
265
+ # Add a few related local files as read-only context to reduce back-and-forth.
266
+ ctx_pairs: list[tuple[str, str]] = []
267
+ if context_files and context_files > 0:
268
+ rels: list[str] = []
269
+ for abs_p in find_imports(target_file, sandbox_root):
270
+ try:
271
+ rels.append(os.path.relpath(abs_p, sandbox_root))
272
+ except Exception:
273
+ continue
274
+ # drop duplicates and the target itself
275
+ rels = [r for r in rels if r not in hint_paths]
276
+ for rel in rels[:context_files]:
277
+ abs_p = os.path.join(sandbox_root, rel)
278
+ if os.path.exists(abs_p):
279
+ try:
280
+ ctx_pairs.append((rel, safe_read(abs_p)))
281
+ except Exception:
282
+ pass
283
+
284
+ raw = fix_code_with_gpt_patch_protocol(
285
+ sandbox_root=sandbox_root,
286
+ error_log=stderr,
287
+ api_key=api_key,
288
+ model=model,
289
+ hint_paths=hint_paths,
290
+ context_files=ctx_pairs,
291
+ )
292
+
293
+ patch_files = parse_patch_protocol_response(raw)
294
+
295
+ if len(patch_files) > max_files_changed:
296
+ raise ValueError(
297
+ f"Patch wants to change {len(patch_files)} files, exceeding --max-files-changed={max_files_changed}"
298
+ )
299
+
300
+ resolved = validate_and_resolve_patch_files(patch_files, sandbox_root=sandbox_root)
301
+
302
+ # Prepare diffs / approvals and apply atomically.
303
+ sha_by_rel = {pf.path: pf.sha256 for pf in patch_files}
304
+
305
+ planned: list[tuple[str, str, str, str]] = [] # (abs_path, rel, old, new)
306
+ for abs_path, new_content in resolved:
307
+ old = ""
308
+ if os.path.exists(abs_path):
309
+ old = safe_read(abs_path)
310
+ if new_content.strip() == (old or "").strip():
311
+ continue
312
+ rel = os.path.relpath(abs_path, sandbox_root)
313
+ planned.append((abs_path, rel, old, new_content))
314
+
315
+ if planned:
316
+ if len(planned) > max_files_changed:
317
+ raise ValueError(
318
+ f"Patch would change {len(planned)} files, exceeding --max-files-changed={max_files_changed}"
319
+ )
320
+
321
+ if approve:
322
+ print("\nPROPOSED CHANGES (patch protocol):")
323
+ for _, rel, old, new in planned:
324
+ diff = "".join(
325
+ difflib.unified_diff(
326
+ (old or "").splitlines(keepends=True),
327
+ (new or "").splitlines(keepends=True),
328
+ fromfile=rel + " (before)",
329
+ tofile=rel + " (after)",
330
+ )
331
+ )
332
+ print(diff)
333
+
334
+ confirm = input("Apply these changes in the sandbox? (y/n): ").strip().lower()
335
+ if confirm != "y":
336
+ log("User declined patch application", "WARN")
337
+ planned = []
338
+
339
+ for abs_path, rel, old, new_content in planned:
340
+ expected_sha = sha_by_rel.get(rel)
341
+ if not expected_sha:
342
+ # Shouldn't happen; keep safe.
343
+ raise ValueError(f"Missing sha256 for {rel} in patch protocol payload")
344
+
345
+ atomic_write_verified_sha256(abs_path, new_content, expected_sha)
346
+ changed_sandbox_files.add(os.path.abspath(abs_path))
347
+ applied_any = True
348
+
349
+ except Exception as e:
350
+ log(f"Patch protocol failed ({e}); falling back to full-text mode", "WARN")
351
+
352
+ if not applied_any:
353
+ fixed_code = fix_code_with_gpt(
354
+ original_code=open(target_file, encoding="utf-8").read(),
355
+ error_log=stderr,
356
+ api_key=api_key,
357
+ model=model,
358
+ )
359
+
360
+ if fixed_code.strip() == open(target_file, encoding="utf-8").read().strip():
361
+ log("GPT returned no changes. Stopping.", "WARN")
362
+ break
363
+
364
+ if approve:
365
+ import difflib
366
+
367
+ old = open(target_file, encoding="utf-8").read()
368
+ diff = "".join(
369
+ difflib.unified_diff(
370
+ (old or "").splitlines(keepends=True),
371
+ (fixed_code or "").splitlines(keepends=True),
372
+ fromfile=os.path.relpath(target_file, sandbox_root) + " (before)",
373
+ tofile=os.path.relpath(target_file, sandbox_root) + " (after)",
374
+ )
375
+ )
376
+ print("\nPROPOSED CHANGES (legacy mode):")
377
+ print(diff)
378
+ confirm = input("Apply this change in the sandbox? (y/n): ").strip().lower()
379
+ if confirm != "y":
380
+ log("User declined patch application", "WARN")
381
+ cleanup_sandbox()
382
+ return False
383
+
384
+ from auto_code_fixer.patcher import safe_write
385
+
386
+ safe_write(target_file, fixed_code)
387
+ changed_sandbox_files.add(os.path.abspath(target_file))
388
+ applied_any = True
389
+
390
+ if applied_any:
391
+ _run_optional_formatters_and_linters()
392
+ log("Code updated by GPT ✏️")
393
+ time.sleep(1)
394
+
395
+ log("Failed to auto-fix file after max retries ❌", "ERROR")
396
+ cleanup_sandbox()
397
+ return False
398
+
399
+
400
+ def main():
401
+ parser = argparse.ArgumentParser(
402
+ description="Auto-fix Python code using OpenAI (advanced sandbox + retry loop)"
403
+ )
404
+
405
+ parser.add_argument(
406
+ "--version",
407
+ action="version",
408
+ version=f"%(prog)s {__version__}",
409
+ )
410
+
411
+ parser.add_argument(
412
+ "entry_file",
413
+ nargs="?",
414
+ help="Path to the main Python file",
415
+ )
416
+
417
+ parser.add_argument("--project-root", default=".")
418
+ parser.add_argument("--api-key")
419
+ parser.add_argument("--model", default=None, help="OpenAI model (default: AUTO_CODE_FIXER_MODEL or gpt-4.1-mini)")
420
+ parser.add_argument("--timeout", type=int, default=30, help="Execution timeout seconds (default: 30)")
421
+ parser.add_argument("--dry-run", action="store_true", help="Do not overwrite files; just report what would change")
422
+ parser.add_argument("--max-retries", type=int, default=DEFAULT_MAX_RETRIES, help="Max fix attempts (default: 8)")
423
+ parser.add_argument(
424
+ "--run",
425
+ default=None,
426
+ help=(
427
+ "Optional command to run instead of `python entry.py`. Examples: 'pytest -q', 'python -m module'. "
428
+ "If set, it runs inside the sandbox venv."
429
+ ),
430
+ )
431
+ parser.add_argument(
432
+ "--ai-plan",
433
+ action="store_true",
434
+ help="Optional: use AI to suggest which file to edit (AUTO_CODE_FIXER_AI_PLAN=1)",
435
+ )
436
+
437
+ parser.add_argument(
438
+ "--legacy-mode",
439
+ action="store_true",
440
+ help=(
441
+ "Disable the default JSON patch protocol and use legacy full-text edit mode only. "
442
+ "(Not recommended; patch protocol is safer and supports multi-file fixes.)"
443
+ ),
444
+ )
445
+
446
+ parser.add_argument(
447
+ "--max-files-changed",
448
+ type=int,
449
+ default=20,
450
+ help="Safety guard: maximum number of files the model may change per attempt (default: 20)",
451
+ )
452
+
453
+ parser.add_argument(
454
+ "--context-files",
455
+ type=int,
456
+ default=3,
457
+ help=(
458
+ "Include up to N related local files (imports) as read-only context in the LLM prompt "
459
+ "when using patch protocol (default: 3)"
460
+ ),
461
+ )
462
+
463
+ parser.add_argument(
464
+ "--approve",
465
+ action="store_true",
466
+ help="Show diffs for proposed changes and ask for approval before applying them in the sandbox",
467
+ )
468
+
469
+ parser.add_argument(
470
+ "--format",
471
+ default=None,
472
+ choices=["black"],
473
+ help="Optional: run formatter in sandbox (best-effort). Currently supported: black",
474
+ )
475
+ parser.add_argument(
476
+ "--lint",
477
+ default=None,
478
+ choices=["ruff"],
479
+ help="Optional: run linter in sandbox (best-effort). Currently supported: ruff",
480
+ )
481
+ parser.add_argument(
482
+ "--fix",
483
+ action="store_true",
484
+ help="If used with --lint ruff, apply fixes (ruff --fix) (best-effort)",
485
+ )
486
+
487
+ # ✅ Proper boolean flags
488
+ ask_group = parser.add_mutually_exclusive_group()
489
+ ask_group.add_argument(
490
+ "--ask",
491
+ action="store_true",
492
+ help="Ask before overwriting files",
493
+ )
494
+ ask_group.add_argument(
495
+ "--no-ask",
496
+ action="store_true",
497
+ help="Do not ask before overwriting files",
498
+ )
499
+
500
+ parser.add_argument(
501
+ "--verbose",
502
+ action="store_true",
503
+ help="Enable verbose/debug output",
504
+ )
505
+
506
+ args = parser.parse_args()
507
+
508
+ if not args.entry_file:
509
+ parser.error("the following arguments are required: entry_file")
510
+
511
+ # ENV defaults
512
+ env_ask = config("AUTO_CODE_FIXER_ASK", default=False, cast=bool)
513
+ env_verbose = config("AUTO_CODE_FIXER_VERBOSE", default=False, cast=bool)
514
+
515
+ # Final ask resolution (CLI overrides ENV)
516
+ if args.ask:
517
+ ask = True
518
+ elif args.no_ask:
519
+ ask = False
520
+ else:
521
+ ask = env_ask
522
+
523
+ verbose = args.verbose or env_verbose
524
+ set_verbose(verbose)
525
+
526
+ # Optional: enable AI planning helper
527
+ if args.ai_plan:
528
+ os.environ["AUTO_CODE_FIXER_AI_PLAN"] = "1"
529
+
530
+ ok = fix_file(
531
+ args.entry_file,
532
+ args.project_root,
533
+ args.api_key,
534
+ ask,
535
+ verbose,
536
+ dry_run=args.dry_run,
537
+ model=args.model,
538
+ timeout_s=args.timeout,
539
+ max_retries=args.max_retries,
540
+ run_cmd=args.run,
541
+ patch_protocol=not args.legacy_mode,
542
+ max_files_changed=args.max_files_changed,
543
+ context_files=args.context_files,
544
+ approve=args.approve,
545
+ fmt=args.format,
546
+ lint=args.lint,
547
+ lint_fix=args.fix,
548
+ )
549
+
550
+ raise SystemExit(0 if ok else 2)
551
+
552
+
553
+ if __name__ == "__main__":
554
+ main()