feat: add automated scripts for Tailwind color migration with batch processing and verification

This commit is contained in:
senke 2026-01-16 01:54:57 +01:00
parent 01f2acc718
commit e072f2539b
3 changed files with 779 additions and 0 deletions

View file

@ -0,0 +1,123 @@
# Automated Tailwind Color Migration Scripts
This directory contains scripts to automatically migrate Tailwind default colors to the Kodo design system.
## Scripts
### 1. `auto_migrate_tailwind_colors.py`
Single-file migration script with verification.
**Usage:**
```bash
# Dry run (test without modifying files)
python3 scripts/auto_migrate_tailwind_colors.py --dry-run
# Migrate all files
python3 scripts/auto_migrate_tailwind_colors.py
# Migrate first 10 files only
python3 scripts/auto_migrate_tailwind_colors.py --limit 10
# Verify files only (check for remaining instances)
python3 scripts/auto_migrate_tailwind_colors.py --verify
```
### 2. `auto_migrate_tailwind_colors_batch.py` ⭐ **RECOMMENDED**
Batch migration script that processes files in batches and commits after each batch.
**Usage:**
```bash
# Dry run with batches of 10 files
python3 scripts/auto_migrate_tailwind_colors_batch.py --dry-run --batch-size 10
# Migrate all files in batches of 10 (auto-commits each batch)
python3 scripts/auto_migrate_tailwind_colors_batch.py --batch-size 10
# Migrate with smaller batches (more commits, safer)
python3 scripts/auto_migrate_tailwind_colors_batch.py --batch-size 5
```
### 3. `generate_tailwind_list.py`
Regenerates the full instance list from the codebase.
**Usage:**
```bash
python3 scripts/generate_tailwind_list.py
```
## Recommended Workflow
1. **Test with dry-run first:**
```bash
python3 scripts/auto_migrate_tailwind_colors_batch.py --dry-run --batch-size 10
```
2. **Run migration in small batches:**
```bash
# Start with 5 files per batch for safety
python3 scripts/auto_migrate_tailwind_colors_batch.py --batch-size 5
```
3. **Verify after migration:**
```bash
python3 scripts/auto_migrate_tailwind_colors.py --verify
```
4. **If issues found, regenerate list:**
```bash
python3 scripts/generate_tailwind_list.py
```
## What the Scripts Do
1. **Read** `apps/web/docs/TAILWIND_INSTANCES_FULL_LIST.md` to find all instances
2. **Parse** each file and find Tailwind default color classes
3. **Replace** with Kodo design system colors based on mapping:
- `text-gray-400``text-kodo-content-dim`
- `bg-gray-700``bg-kodo-steel`
- `border-gray-600``border-kodo-steel`
- `text-blue-600``text-kodo-cyan`
- etc.
4. **Verify** each file has no remaining Tailwind default colors
5. **Commit** changes in batches (batch script only)
## Color Mappings
The scripts use the following mappings (see `TAILWIND_COLORS_AUDIT.md` for full details):
- **Gray text**: `text-gray-400/500/600``text-kodo-content-dim`
- **Gray text (dark)**: `text-gray-300/700/800/900``text-kodo-text-main`
- **Gray backgrounds**: `bg-gray-*``bg-kodo-void/ink/graphite/slate/steel`
- **Gray borders**: `border-gray-*``border-kodo-steel`
- **Blue colors**: `text-blue-*``text-kodo-cyan`
- **Red colors**: `text-red-*``text-kodo-red`
- **Green colors**: `text-green-*``text-kodo-lime`
## Safety Features
- ✅ Dry-run mode to preview changes
- ✅ Verification after each file
- ✅ Batch processing with commits
- ✅ Detailed logging of all changes
- ✅ Error handling for file I/O issues
## Troubleshooting
**Script finds instances but doesn't migrate them:**
- Check if the color class is in the `COLOR_MAPPINGS` dictionary
- Some edge cases (conditional classes, template strings) may need manual review
**Verification fails after migration:**
- Some instances might be in comments or strings (not actual classes)
- Check the file manually and fix any remaining issues
**Git commit fails:**
- Ensure you're in the repository root
- Check git status for uncommitted changes
- The script will continue even if commit fails (you can commit manually)
## Notes
- The scripts exclude test files (`*.test.tsx`, `*.test.ts`) and backup files (`ui.backup/`)
- Dark mode classes (e.g., `dark:bg-gray-900`) are handled automatically
- The batch script commits with format: `consistency: auto-migrate Tailwind default colors (Batch N, X instances)`

View file

@ -0,0 +1,342 @@
#!/usr/bin/env python3
"""
Automated script to migrate all Tailwind default colors to Kodo design system colors.
Uses TAILWIND_INSTANCES_FULL_LIST.md to find all instances and migrate them automatically.
"""
import re
import sys
from pathlib import Path
from typing import Dict, List, Tuple, Optional
from collections import defaultdict
# Color mapping from Tailwind to Kodo design system
COLOR_MAPPINGS = {
# Gray text colors
'text-gray-50': 'text-kodo-text-main',
'text-gray-100': 'text-kodo-text-main',
'text-gray-200': 'text-kodo-text-main',
'text-gray-300': 'text-kodo-text-main',
'text-gray-400': 'text-kodo-content-dim',
'text-gray-500': 'text-kodo-content-dim',
'text-gray-600': 'text-kodo-content-dim',
'text-gray-700': 'text-kodo-text-main',
'text-gray-800': 'text-kodo-text-main',
'text-gray-900': 'text-kodo-text-main',
# Gray background colors
'bg-gray-50': 'bg-kodo-void',
'bg-gray-100': 'bg-kodo-void',
'bg-gray-200': 'bg-kodo-slate',
'bg-gray-300': 'bg-kodo-slate',
'bg-gray-400': 'bg-kodo-steel',
'bg-gray-500': 'bg-kodo-steel',
'bg-gray-600': 'bg-kodo-steel',
'bg-gray-700': 'bg-kodo-steel',
'bg-gray-800': 'bg-kodo-graphite',
'bg-gray-900': 'bg-kodo-ink',
# Gray border colors
'border-gray-200': 'border-kodo-steel',
'border-gray-300': 'border-kodo-steel',
'border-gray-400': 'border-kodo-steel',
'border-gray-500': 'border-kodo-steel',
'border-gray-600': 'border-kodo-steel',
'border-gray-700': 'border-kodo-steel',
'border-gray-800': 'border-kodo-steel',
# Blue colors (map to cyan when appropriate)
'text-blue-50': 'text-kodo-cyan',
'text-blue-100': 'text-kodo-cyan',
'text-blue-200': 'text-kodo-cyan',
'text-blue-300': 'text-kodo-cyan',
'text-blue-400': 'text-kodo-cyan',
'text-blue-500': 'text-kodo-cyan',
'text-blue-600': 'text-kodo-cyan',
'text-blue-700': 'text-kodo-cyan',
'text-blue-800': 'text-kodo-cyan',
'text-blue-900': 'text-kodo-cyan',
'bg-blue-50': 'bg-kodo-cyan/10',
'bg-blue-100': 'bg-kodo-cyan/20',
'bg-blue-200': 'bg-kodo-cyan/30',
'bg-blue-300': 'bg-kodo-cyan/40',
'bg-blue-400': 'bg-kodo-cyan',
'bg-blue-500': 'bg-kodo-cyan',
'bg-blue-600': 'bg-kodo-cyan',
'bg-blue-700': 'bg-kodo-cyan',
'bg-blue-800': 'bg-kodo-cyan',
'bg-blue-900': 'bg-kodo-cyan',
'border-blue-200': 'border-kodo-cyan',
'border-blue-300': 'border-kodo-cyan',
'border-blue-400': 'border-kodo-cyan',
'border-blue-500': 'border-kodo-cyan',
'border-blue-600': 'border-kodo-cyan',
# Red colors (map to kodo-red when appropriate)
'text-red-400': 'text-kodo-red',
'text-red-500': 'text-kodo-red',
'text-red-600': 'text-kodo-red',
'text-red-700': 'text-kodo-red',
'bg-red-400': 'bg-kodo-red',
'bg-red-500': 'bg-kodo-red',
'bg-red-600': 'bg-kodo-red',
'bg-red-700': 'bg-kodo-red',
'border-red-200': 'border-kodo-red',
'border-red-300': 'border-kodo-red',
'border-red-400': 'border-kodo-red',
'border-red-500': 'border-kodo-red',
'border-red-600': 'border-kodo-red',
# Green colors (map to kodo-lime when appropriate)
'text-green-400': 'text-kodo-lime',
'text-green-500': 'text-kodo-lime',
'text-green-600': 'text-kodo-lime',
'text-green-700': 'text-kodo-lime',
'bg-green-50': 'bg-kodo-lime/10',
'bg-green-100': 'bg-kodo-lime/20',
'bg-green-200': 'bg-kodo-lime/30',
'border-green-200': 'border-kodo-lime',
'border-green-300': 'border-kodo-lime',
# Gradient colors
'from-gray-700': 'from-kodo-steel',
'from-gray-800': 'from-kodo-graphite',
'from-gray-900': 'from-kodo-ink',
'to-gray-700': 'to-kodo-steel',
'to-gray-800': 'to-kodo-graphite',
'to-gray-900': 'to-kodo-ink',
'to-blue-600': 'to-kodo-cyan',
'from-blue-600': 'from-kodo-cyan',
}
# Pattern to match Tailwind default colors
TAILWIND_PATTERN = re.compile(
r'\b(' + '|'.join(re.escape(k) for k in COLOR_MAPPINGS.keys()) + r')\b'
)
def parse_instance_list(list_file: Path) -> Dict[str, List[Tuple[int, str]]]:
"""Parse the TAILWIND_INSTANCES_FULL_LIST.md file."""
instances = defaultdict(list)
if not list_file.exists():
print(f"Error: {list_file} not found")
return instances
current_file = None
with open(list_file, 'r', encoding='utf-8') as f:
for line in f:
# Match file header: ### apps/web/src/...
file_match = re.match(r'^### (.+)$', line)
if file_match:
current_file = file_match.group(1)
continue
# Match line entry: - **Line 123**: `text-gray-400`
line_match = re.match(r'^- \*\*Line (\d+)\*\*:', line)
if line_match and current_file:
line_num = int(line_match.group(1))
instances[current_file].append(line_num)
return instances
def migrate_file(file_path: Path, dry_run: bool = False) -> Tuple[int, List[str]]:
"""Migrate a single file, returning count of changes and list of changes."""
if not file_path.exists():
return 0, [f"File not found: {file_path}"]
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
return 0, [f"Error reading file: {e}"]
original_content = content
changes = []
change_count = 0
# Find all matches and replace them
def replace_match(match):
nonlocal change_count
original = match.group(0)
if original in COLOR_MAPPINGS:
replacement = COLOR_MAPPINGS[original]
if original != replacement:
change_count += 1
changes.append(f" Line {get_line_number(content, match.start())}: {original}{replacement}")
return replacement
return original
# Replace all instances
new_content = TAILWIND_PATTERN.sub(replace_match, content)
# Also handle cases where colors might be in template strings or conditional expressions
# This is a more aggressive pass for edge cases
for original, replacement in COLOR_MAPPINGS.items():
if original in new_content and original != replacement:
# Count additional occurrences that might have been missed
count = new_content.count(original)
if count > 0:
new_content = new_content.replace(original, replacement)
change_count += count
changes.append(f" Additional replacements: {original}{replacement} ({count} times)")
# Write the file if there were changes and not dry run
if new_content != original_content and not dry_run:
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
except Exception as e:
return 0, [f"Error writing file: {e}"]
return change_count, changes
def get_line_number(content: str, position: int) -> int:
"""Get line number from character position."""
return content[:position].count('\n') + 1
def verify_file(file_path: Path) -> Tuple[bool, List[str]]:
"""Verify that a file has no remaining Tailwind default colors."""
if not file_path.exists():
return False, ["File not found"]
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
return False, [f"Error reading file: {e}"]
issues = []
for line_num, line in enumerate(content.split('\n'), 1):
# Check for any Tailwind default colors
matches = TAILWIND_PATTERN.findall(line)
if matches:
issues.append(f"Line {line_num}: Still contains {', '.join(matches)}")
return len(issues) == 0, issues
def main():
"""Main execution function."""
root = Path(__file__).parent.parent
list_file = root / 'apps' / 'web' / 'docs' / 'TAILWIND_INSTANCES_FULL_LIST.md'
src_root = root / 'apps' / 'web' / 'src'
# Parse command line arguments
dry_run = '--dry-run' in sys.argv
verify_only = '--verify' in sys.argv
file_limit = None
if '--limit' in sys.argv:
idx = sys.argv.index('--limit')
if idx + 1 < len(sys.argv):
file_limit = int(sys.argv[idx + 1])
if verify_only:
print("=== VERIFICATION MODE ===")
print("Checking files for remaining Tailwind default colors...\n")
elif dry_run:
print("=== DRY RUN MODE ===")
print("Will show changes without modifying files...\n")
else:
print("=== MIGRATION MODE ===")
print("Migrating Tailwind default colors to Kodo design system...\n")
# Parse instance list
print(f"Reading instance list from: {list_file}")
instances = parse_instance_list(list_file)
print(f"Found {len(instances)} files with instances\n")
if file_limit:
print(f"Limiting to first {file_limit} files\n")
instances = dict(list(instances.items())[:file_limit])
# Process each file
total_changes = 0
files_processed = 0
files_with_errors = []
files_verified = []
files_failed_verification = []
for file_path_str, line_numbers in sorted(instances.items()):
file_path = root / file_path_str
if not file_path.exists():
print(f"⚠️ {file_path_str}: File not found")
files_with_errors.append(file_path_str)
continue
files_processed += 1
print(f"[{files_processed}/{len(instances)}] Processing: {file_path_str}")
print(f" Expected instances on lines: {', '.join(map(str, line_numbers))}")
if verify_only:
is_valid, issues = verify_file(file_path)
if is_valid:
print(f" ✅ Verified: No Tailwind default colors found")
files_verified.append(file_path_str)
else:
print(f" ❌ Failed verification:")
for issue in issues:
print(f" {issue}")
files_failed_verification.append(file_path_str)
else:
change_count, changes = migrate_file(file_path, dry_run=dry_run)
total_changes += change_count
if change_count > 0:
print(f" ✅ Migrated {change_count} instance(s):")
for change in changes[:5]: # Show first 5 changes
print(change)
if len(changes) > 5:
print(f" ... and {len(changes) - 5} more")
else:
print(f" ⚠️ No changes made (may already be migrated)")
# Verify after migration
if not dry_run:
is_valid, issues = verify_file(file_path)
if is_valid:
print(f" ✅ Verified: No remaining Tailwind default colors")
files_verified.append(file_path_str)
else:
print(f" ⚠️ Verification found remaining issues:")
for issue in issues[:3]: # Show first 3 issues
print(f" {issue}")
if len(issues) > 3:
print(f" ... and {len(issues) - 3} more")
files_failed_verification.append(file_path_str)
print()
# Summary
print("=" * 60)
print("SUMMARY")
print("=" * 60)
print(f"Files processed: {files_processed}")
if not verify_only:
print(f"Total changes made: {total_changes}")
print(f"Files verified: {len(files_verified)}")
if files_failed_verification:
print(f"Files with remaining issues: {len(files_failed_verification)}")
for f in files_failed_verification[:10]:
print(f" - {f}")
if len(files_failed_verification) > 10:
print(f" ... and {len(files_failed_verification) - 10} more")
if files_with_errors:
print(f"Files with errors: {len(files_with_errors)}")
for f in files_with_errors[:5]:
print(f" - {f}")
if verify_only:
if files_failed_verification:
sys.exit(1)
elif not dry_run and files_failed_verification:
print("\n⚠️ Some files still have remaining Tailwind default colors.")
print(" Please review and fix manually.")
sys.exit(1)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,314 @@
#!/usr/bin/env python3
"""
Automated batch migration script for Tailwind default colors.
Processes files in batches, commits after each batch, and verifies all changes.
"""
import re
import sys
import subprocess
from pathlib import Path
from typing import Dict, List, Tuple
from collections import defaultdict
# Import color mappings from the main script
# (In a real scenario, we'd import from the main script, but for simplicity, we'll duplicate)
COLOR_MAPPINGS = {
'text-gray-50': 'text-kodo-text-main',
'text-gray-100': 'text-kodo-text-main',
'text-gray-200': 'text-kodo-text-main',
'text-gray-300': 'text-kodo-text-main',
'text-gray-400': 'text-kodo-content-dim',
'text-gray-500': 'text-kodo-content-dim',
'text-gray-600': 'text-kodo-content-dim',
'text-gray-700': 'text-kodo-text-main',
'text-gray-800': 'text-kodo-text-main',
'text-gray-900': 'text-kodo-text-main',
'bg-gray-50': 'bg-kodo-void',
'bg-gray-100': 'bg-kodo-void',
'bg-gray-200': 'bg-kodo-slate',
'bg-gray-300': 'bg-kodo-slate',
'bg-gray-400': 'bg-kodo-steel',
'bg-gray-500': 'bg-kodo-steel',
'bg-gray-600': 'bg-kodo-steel',
'bg-gray-700': 'bg-kodo-steel',
'bg-gray-800': 'bg-kodo-graphite',
'bg-gray-900': 'bg-kodo-ink',
'border-gray-200': 'border-kodo-steel',
'border-gray-300': 'border-kodo-steel',
'border-gray-400': 'border-kodo-steel',
'border-gray-500': 'border-kodo-steel',
'border-gray-600': 'border-kodo-steel',
'border-gray-700': 'border-kodo-steel',
'border-gray-800': 'border-kodo-steel',
'text-blue-50': 'text-kodo-cyan',
'text-blue-100': 'text-kodo-cyan',
'text-blue-200': 'text-kodo-cyan',
'text-blue-300': 'text-kodo-cyan',
'text-blue-400': 'text-kodo-cyan',
'text-blue-500': 'text-kodo-cyan',
'text-blue-600': 'text-kodo-cyan',
'text-blue-700': 'text-kodo-cyan',
'text-blue-800': 'text-kodo-cyan',
'text-blue-900': 'text-kodo-cyan',
'bg-blue-50': 'bg-kodo-cyan/10',
'bg-blue-100': 'bg-kodo-cyan/20',
'bg-blue-200': 'bg-kodo-cyan/30',
'bg-blue-300': 'bg-kodo-cyan/40',
'bg-blue-400': 'bg-kodo-cyan',
'bg-blue-500': 'bg-kodo-cyan',
'bg-blue-600': 'bg-kodo-cyan',
'bg-blue-700': 'bg-kodo-cyan',
'bg-blue-800': 'bg-kodo-cyan',
'bg-blue-900': 'bg-kodo-cyan',
'border-blue-200': 'border-kodo-cyan',
'border-blue-300': 'border-kodo-cyan',
'border-blue-400': 'border-kodo-cyan',
'border-blue-500': 'border-kodo-cyan',
'border-blue-600': 'border-kodo-cyan',
'text-red-400': 'text-kodo-red',
'text-red-500': 'text-kodo-red',
'text-red-600': 'text-kodo-red',
'text-red-700': 'text-kodo-red',
'bg-red-400': 'bg-kodo-red',
'bg-red-500': 'bg-kodo-red',
'bg-red-600': 'bg-kodo-red',
'bg-red-700': 'bg-kodo-red',
'border-red-200': 'border-kodo-red',
'border-red-300': 'border-kodo-red',
'border-red-400': 'border-kodo-red',
'border-red-500': 'border-kodo-red',
'border-red-600': 'border-kodo-red',
'text-green-400': 'text-kodo-lime',
'text-green-500': 'text-kodo-lime',
'text-green-600': 'text-kodo-lime',
'text-green-700': 'text-kodo-lime',
'bg-green-50': 'bg-kodo-lime/10',
'bg-green-100': 'bg-kodo-lime/20',
'bg-green-200': 'bg-kodo-lime/30',
'border-green-200': 'border-kodo-lime',
'border-green-300': 'border-kodo-lime',
'from-gray-700': 'from-kodo-steel',
'from-gray-800': 'from-kodo-graphite',
'from-gray-900': 'from-kodo-ink',
'to-gray-700': 'to-kodo-steel',
'to-gray-800': 'to-kodo-graphite',
'to-gray-900': 'to-kodo-ink',
'to-blue-600': 'to-kodo-cyan',
'from-blue-600': 'from-kodo-cyan',
}
TAILWIND_PATTERN = re.compile(
r'\b(' + '|'.join(re.escape(k) for k in COLOR_MAPPINGS.keys()) + r')\b'
)
def parse_instance_list(list_file: Path) -> Dict[str, List[int]]:
"""Parse the TAILWIND_INSTANCES_FULL_LIST.md file."""
instances = defaultdict(list)
if not list_file.exists():
print(f"Error: {list_file} not found")
return instances
current_file = None
with open(list_file, 'r', encoding='utf-8') as f:
for line in f:
file_match = re.match(r'^### (.+)$', line)
if file_match:
current_file = file_match.group(1)
continue
line_match = re.match(r'^- \*\*Line (\d+)\*\*:', line)
if line_match and current_file:
line_num = int(line_match.group(1))
instances[current_file].append(line_num)
return instances
def migrate_file(file_path: Path, dry_run: bool = False) -> Tuple[int, List[str]]:
"""Migrate a single file."""
if not file_path.exists():
return 0, [f"File not found: {file_path}"]
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
return 0, [f"Error reading file: {e}"]
original_content = content
changes = []
change_count = 0
def replace_match(match):
nonlocal change_count
original = match.group(0)
if original in COLOR_MAPPINGS:
replacement = COLOR_MAPPINGS[original]
if original != replacement:
change_count += 1
line_num = content[:match.start()].count('\n') + 1
changes.append(f"Line {line_num}: {original}{replacement}")
return replacement
return original
new_content = TAILWIND_PATTERN.sub(replace_match, content)
# Additional pass for any missed instances
for original, replacement in COLOR_MAPPINGS.items():
if original in new_content and original != replacement:
count = new_content.count(original)
if count > 0:
new_content = new_content.replace(original, replacement)
change_count += count
if new_content != original_content and not dry_run:
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(new_content)
except Exception as e:
return 0, [f"Error writing file: {e}"]
return change_count, changes
def verify_file(file_path: Path) -> Tuple[bool, List[str]]:
"""Verify that a file has no remaining Tailwind default colors."""
if not file_path.exists():
return False, ["File not found"]
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
return False, [f"Error reading file: {e}"]
issues = []
for line_num, line in enumerate(content.split('\n'), 1):
matches = TAILWIND_PATTERN.findall(line)
if matches:
issues.append(f"Line {line_num}: Still contains {', '.join(matches)}")
return len(issues) == 0, issues
def git_commit(files: List[str], batch_num: int, total_changes: int) -> bool:
"""Commit the changes."""
try:
# Add files
subprocess.run(['git', 'add'] + files, check=True, capture_output=True)
# Commit
commit_msg = f"consistency: auto-migrate Tailwind default colors (Batch {batch_num}, {total_changes} instances)"
result = subprocess.run(
['git', 'commit', '-m', commit_msg],
check=True,
capture_output=True,
text=True
)
return True
except subprocess.CalledProcessError as e:
print(f" ⚠️ Git commit failed: {e.stderr.decode() if e.stderr else str(e)}")
return False
def main():
"""Main execution function."""
root = Path(__file__).parent.parent
list_file = root / 'apps' / 'web' / 'docs' / 'TAILWIND_INSTANCES_FULL_LIST.md'
batch_size = 10
if '--batch-size' in sys.argv:
idx = sys.argv.index('--batch-size')
if idx + 1 < len(sys.argv):
batch_size = int(sys.argv[idx + 1])
dry_run = '--dry-run' in sys.argv
print("=== AUTOMATED BATCH MIGRATION ===")
if dry_run:
print("DRY RUN MODE - No files will be modified\n")
else:
print(f"Processing files in batches of {batch_size}\n")
# Parse instance list
print(f"Reading instance list from: {list_file}")
instances = parse_instance_list(list_file)
print(f"Found {len(instances)} files with instances\n")
# Process in batches
all_files = sorted(instances.keys())
total_files = len(all_files)
batch_num = 0
total_changes_all = 0
files_verified = []
files_failed = []
for i in range(0, total_files, batch_size):
batch_num += 1
batch_files = all_files[i:i + batch_size]
print(f"\n{'='*60}")
print(f"BATCH {batch_num} ({len(batch_files)} files)")
print(f"{'='*60}\n")
batch_changes = 0
batch_file_paths = []
for file_path_str in batch_files:
file_path = root / file_path_str
if not file_path.exists():
print(f"⚠️ {file_path_str}: File not found")
continue
print(f" Processing: {file_path_str}")
change_count, changes = migrate_file(file_path, dry_run=dry_run)
batch_changes += change_count
batch_file_paths.append(file_path_str)
if change_count > 0:
print(f" ✅ Migrated {change_count} instance(s)")
else:
print(f" ⚠️ No changes (may already be migrated)")
# Verify
is_valid, issues = verify_file(file_path)
if is_valid:
print(f" ✅ Verified: No remaining Tailwind default colors")
files_verified.append(file_path_str)
else:
print(f" ⚠️ Verification issues: {len(issues)} remaining")
files_failed.append((file_path_str, issues))
total_changes_all += batch_changes
print(f"\n Batch {batch_num} Summary:")
print(f" Files processed: {len(batch_file_paths)}")
print(f" Total changes: {batch_changes}")
print(f" Files verified: {len([f for f in batch_file_paths if f in files_verified])}")
# Commit batch
if not dry_run and batch_changes > 0:
print(f"\n Committing batch {batch_num}...")
if git_commit(batch_file_paths, batch_num, batch_changes):
print(f" ✅ Committed batch {batch_num}")
else:
print(f" ⚠️ Failed to commit batch {batch_num}")
# Final summary
print(f"\n{'='*60}")
print("FINAL SUMMARY")
print(f"{'='*60}")
print(f"Total batches: {batch_num}")
print(f"Total files processed: {total_files}")
print(f"Total changes made: {total_changes_all}")
print(f"Files verified: {len(files_verified)}")
print(f"Files with issues: {len(files_failed)}")
if files_failed:
print(f"\nFiles that need manual review:")
for file_path, issues in files_failed[:10]:
print(f" - {file_path} ({len(issues)} issues)")
if len(files_failed) > 10:
print(f" ... and {len(files_failed) - 10} more")
if __name__ == '__main__':
main()