feat: add automated scripts for Tailwind color migration with batch processing and verification
This commit is contained in:
parent
01f2acc718
commit
e072f2539b
3 changed files with 779 additions and 0 deletions
123
scripts/README_TAILWIND_MIGRATION.md
Normal file
123
scripts/README_TAILWIND_MIGRATION.md
Normal 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)`
|
||||||
342
scripts/auto_migrate_tailwind_colors.py
Executable file
342
scripts/auto_migrate_tailwind_colors.py
Executable 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()
|
||||||
314
scripts/auto_migrate_tailwind_colors_batch.py
Executable file
314
scripts/auto_migrate_tailwind_colors_batch.py
Executable 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()
|
||||||
Loading…
Reference in a new issue