-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauto_fix_dates.py
More file actions
196 lines (151 loc) · 6.15 KB
/
auto_fix_dates.py
File metadata and controls
196 lines (151 loc) · 6.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# Created by gpt-5.2 | 2026-02-09_01
"""auto_fix_dates
Walks the local `{scaffold_folder}` directory recursively and forces all files to have a
fresh modification time by making a minimal, semantics-preserving whitespace
change.
Rules implemented (per request):
- For `.md` files: append a single space to the very end of the file.
- For scripting-style files: add a single space to an existing comment line; if
no comment is found, append a new comment line.
- For all other text-like files: append a single space to end of file.
If a file appears binary, the script will only `os.utime()` it (to avoid
corrupting binary content) while still meeting the goal of updating modified
times.
"""
from __future__ import annotations
import argparse
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Optional
@dataclass(frozen=True)
class FixResult:
"""Per-file outcome."""
path: Path
action: str
error: Optional[str] = None
# Created by gpt-5.2 | 2026-02-09_01
def _is_probably_binary(data: bytes) -> bool:
"""Heuristic to avoid editing binary files."""
if not data:
return False
if b"\x00" in data:
return True
# If the first chunk has a high ratio of non-text bytes, treat as binary.
sample = data[:4096]
textish = b"\t\n\r" + bytes(range(32, 127))
non_text = sum(1 for b in sample if b not in textish)
return (non_text / max(1, len(sample))) > 0.25
# Created by gpt-5.2 | 2026-02-09_01
def _comment_prefix_for_suffix(suffix: str) -> Optional[bytes]:
"""Return a line-comment prefix for common scripting/text formats."""
suffix = suffix.lower()
if suffix in {".py", ".sh", ".bash", ".ps1", ".rb"}:
return b"#"
if suffix in {".js", ".ts", ".jsx", ".tsx", ".java", ".c", ".cc", ".cpp", ".h", ".hpp", ".cs", ".go"}:
return b"//"
if suffix in {".sql"}:
return b"--"
return None
# Created by gpt-5.2 | 2026-02-09_01
def _touch_now(path: Path) -> None:
"""Set mtime/atime to now."""
os.utime(path, None)
# Created by gpt-5.2 | 2026-02-09_01
def _append_space_eof(path: Path, data: bytes) -> bytes:
"""Append a single space byte to end-of-file."""
return data + b" "
# Created by gpt-5.2 | 2026-02-09_01
def _add_space_to_comment_line(path: Path, data: bytes, comment_prefix: bytes) -> bytes:
"""Add a single space to an existing comment line; otherwise append one."""
# Work in bytes to avoid encoding changes.
lines = data.splitlines(keepends=True)
for i, line in enumerate(lines):
stripped = line.lstrip()
if stripped.startswith(comment_prefix):
if line.endswith(b"\r\n"):
lines[i] = line[:-2] + b" " + b"\r\n"
elif line.endswith(b"\n"):
lines[i] = line[:-1] + b" " + b"\n"
else:
lines[i] = line + b" "
return b"".join(lines)
# No existing comment found: add a new harmless comment line.
newline = b"\r\n" if b"\r\n" in data else b"\n"
if not data.endswith((b"\n", b"\r\n")) and data:
data = data + newline
return data + comment_prefix + b" " + newline
# Created by gpt-5.2 | 2026-02-09_01
def _fix_one_file(path: Path, *, dry_run: bool) -> FixResult:
"""Apply the requested minimal edit to a single file."""
try:
data = path.read_bytes()
except Exception as e:
return FixResult(path=path, action="read_failed", error=str(e))
if _is_probably_binary(data):
if not dry_run:
try:
_touch_now(path)
except Exception as e:
return FixResult(path=path, action="utime_failed", error=str(e))
return FixResult(path=path, action="binary_utime")
suffix = path.suffix.lower()
if suffix == ".md":
new_data = _append_space_eof(path, data)
action = "md_append_space"
else:
comment_prefix = _comment_prefix_for_suffix(suffix)
if comment_prefix is not None:
new_data = _add_space_to_comment_line(path, data, comment_prefix)
action = "script_comment_space"
else:
new_data = _append_space_eof(path, data)
action = "append_space"
if dry_run:
return FixResult(path=path, action=f"dry_run:{action}")
try:
path.write_bytes(new_data)
_touch_now(path)
return FixResult(path=path, action=action)
except Exception as e:
return FixResult(path=path, action="write_failed", error=str(e))
# Created by gpt-5.2 | 2026-02-09_01
def _iter_scaffold_files(root: Path) -> Iterable[Path]:
"""Yield all files under `{scaffold_folder}/` recursively."""
# Use rglob("*") to include all extensions and nested folders.
for p in root.rglob("*"):
if p.is_file():
yield p
# Created by gpt-5.2 | 2026-02-09_01
def main(argv: Optional[list[str]] = None) -> int:
parser = argparse.ArgumentParser(description="Force {scaffold_folder} file mtimes to now via harmless whitespace edits")
parser.add_argument("--root", default=".kilocode", help="Root folder to process (default: .kilocode)")
parser.add_argument("--dry-run", action="store_true", help="Preview actions without writing")
args = parser.parse_args(argv)
root = Path(args.root)
if not root.exists() or not root.is_dir():
print(f"ERROR: root folder not found or not a directory: {root}")
return 2
files = sorted(_iter_scaffold_files(root))
if not files:
print(f"No files found under: {root}")
return 0
results: list[FixResult] = []
for f in files:
results.append(_fix_one_file(f, dry_run=args.dry_run))
failures = [r for r in results if r.error]
print(f"Processed: {len(results)} files")
print(f"Failures: {len(failures)}")
if failures:
for r in failures:
print(f"- {r.path}: {r.action}: {r.error}")
return 1
# Print a small sample of actions for confidence.
sample = results[:10]
for r in sample:
print(f"- {r.path}: {r.action}")
if len(results) > len(sample):
print(f"... ({len(results) - len(sample)} more)")
return 0
if __name__ == "__main__":
raise SystemExit(main())