diff --git a/scripts/README_TAILWIND_MIGRATION.md b/scripts/README_TAILWIND_MIGRATION.md new file mode 100644 index 000000000..bf1ad8f95 --- /dev/null +++ b/scripts/README_TAILWIND_MIGRATION.md @@ -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)` diff --git a/scripts/auto_migrate_tailwind_colors.py b/scripts/auto_migrate_tailwind_colors.py new file mode 100755 index 000000000..857f77df9 --- /dev/null +++ b/scripts/auto_migrate_tailwind_colors.py @@ -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() diff --git a/scripts/auto_migrate_tailwind_colors_batch.py b/scripts/auto_migrate_tailwind_colors_batch.py new file mode 100755 index 000000000..8a3d70d79 --- /dev/null +++ b/scripts/auto_migrate_tailwind_colors_batch.py @@ -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()