#!/usr/bin/env python3 """ Script to replace decorative/informational kodo-cyan instances with kodo-steel for Action 11.3.1.3: Apply 80/20 rule (80% neutral, 20% color) This script: 1. Finds all instances of kodo-cyan usage 2. Identifies decorative/informational instances 3. Replaces them with kodo-steel variants 4. Preserves active/functional states, design system variants, semantic indicators """ import os import re import sys from pathlib import Path from typing import List, Tuple, Dict # Patterns to find kodo-cyan usage CYAN_PATTERNS = [ r'bg-kodo-cyan/10', r'bg-kodo-cyan/20', r'border-kodo-cyan/30', r'border-kodo-cyan/20', r'border-kodo-cyan', r'text-kodo-cyan', r'text-kodo-cyan-dim', ] # Patterns that indicate functional/active states (PRESERVE) PRESERVE_PATTERNS = [ # Active/selected states - check for conditional logic r'\?.*kodo-cyan.*:', r':.*kodo-cyan.*\?', r'isSelected.*\?.*kodo-cyan', r'isActive.*\?.*kodo-cyan', r'isCurrent.*\?.*kodo-cyan', r'selected.*\?.*kodo-cyan', r'active.*\?.*kodo-cyan', r'current.*\?.*kodo-cyan', r'isPlaying.*\?.*kodo-cyan', r'isFocused.*\?.*kodo-cyan', r'isMe.*\?.*kodo-cyan', r'connected.*\?.*kodo-cyan', r'is_current.*\?.*kodo-cyan', r'draggedIndex.*\?.*kodo-cyan', r'autoScroll.*\?.*kodo-cyan', r'showVisualizer.*\?.*kodo-cyan', r'isQueueOpen.*\?.*kodo-cyan', r'queueTab.*\?.*kodo-cyan', r'paymentMethod.*\?.*kodo-cyan', r'currentTheme.*\?.*kodo-cyan', r'theme ===.*\?.*kodo-cyan', r'dateRange ===.*\?.*kodo-cyan', # Design system variants (PRESERVE) - exact matches r'cyan:\s*[\'"]kodo-cyan', r'color:\s*[\'"]cyan', r'variant:\s*[\'"]cyan', r'variant:\s*[\'"]info', r'severity:\s*[\'"]info', r'badge.*cyan', r'Spinner.*cyan', r'default:\s*[\'"]kodo-cyan', # Semantic indicators (PRESERVE) r'PasswordStrengthIndicator', r'isPrivate.*\?.*kodo-cyan', r'isPublic.*\?.*kodo-cyan', # Functional links (PRESERVE) r'hover:underline', r' bool: """Check if this line should be preserved (functional/active state)""" # Skip test and documentation files if any(pattern in file_path for pattern in SKIP_PATTERNS): return True # Check preserve patterns in current line for pattern in PRESERVE_PATTERNS: if re.search(pattern, line, re.IGNORECASE): return True # Check context (previous and next lines) for conditional logic if context_lines: context = ' '.join(context_lines) # If cyan appears in a ternary or conditional, preserve it if re.search(r'\?.*kodo-cyan|kodo-cyan.*\?', context, re.IGNORECASE): return True # If it's part of a variant/color prop, preserve it if re.search(r'(variant|color|severity).*:.*cyan', context, re.IGNORECASE): return True # Check if line contains conditional logic with cyan if re.search(r'\?.*kodo-cyan|kodo-cyan.*\?', line, re.IGNORECASE): return True # Check if it's a design system prop if re.search(r'(variant|color|severity)\s*[:=]\s*[\'"]?(cyan|info)', line, re.IGNORECASE): return True return False def find_cyan_instances(file_path: Path) -> List[Tuple[int, str, str]]: """Find all kodo-cyan instances in a file""" instances = [] try: with open(file_path, 'r', encoding='utf-8') as f: lines = f.readlines() for line_num, line in enumerate(lines, 1): for pattern in CYAN_PATTERNS: if re.search(pattern, line): instances.append((line_num, line, pattern)) break except Exception as e: print(f"Error reading {file_path}: {e}", file=sys.stderr) return instances def replace_decorative_cyan(file_path: Path, dry_run: bool = False) -> Tuple[int, int]: """Replace decorative cyan instances in a file""" replaced = 0 preserved = 0 try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() original_content = content lines = content.split('\n') new_lines = [] for line_num, line in enumerate(lines, 1): original_line = line # Get context (previous 2 and next 2 lines) context_start = max(0, line_num - 3) context_end = min(len(lines), line_num + 2) context_lines = lines[context_start:context_end] # Check if should preserve if should_preserve(line, str(file_path), context_lines): # Count preserved instances for pattern in CYAN_PATTERNS: if re.search(pattern, line, re.IGNORECASE): preserved += 1 break new_lines.append(line) continue # Replace decorative instances line_replaced = False for old, new in REPLACEMENTS.items(): if old in line: # Double-check we shouldn't preserve if not should_preserve(line, str(file_path), context_lines): line = line.replace(old, new) if not line_replaced: replaced += 1 line_replaced = True new_lines.append(line) new_content = '\n'.join(new_lines) if not dry_run and new_content != original_content: with open(file_path, 'w', encoding='utf-8') as f: f.write(new_content) return replaced, preserved except Exception as e: print(f"Error processing {file_path}: {e}", file=sys.stderr) return 0, 0 def main(): """Main function""" import argparse parser = argparse.ArgumentParser(description='Replace decorative kodo-cyan with kodo-steel') parser.add_argument('--dry-run', action='store_true', help='Show what would be changed without making changes') parser.add_argument('--path', default='apps/web/src', help='Path to search (default: apps/web/src)') args = parser.parse_args() base_path = Path(args.path) if not base_path.exists(): print(f"Error: Path {base_path} does not exist", file=sys.stderr) sys.exit(1) # Find all TypeScript/TSX files tsx_files = [] for ext in ['*.tsx', '*.ts']: tsx_files.extend(base_path.rglob(ext)) # Filter out skipped files files_to_process = [] for file_path in tsx_files: if not any(pattern in str(file_path) for pattern in SKIP_PATTERNS): files_to_process.append(file_path) print(f"Found {len(files_to_process)} files to process") if args.dry_run: print("DRY RUN MODE - No files will be modified\n") total_replaced = 0 total_preserved = 0 files_modified = [] for file_path in sorted(files_to_process): replaced, preserved = replace_decorative_cyan(file_path, dry_run=args.dry_run) if replaced > 0: total_replaced += replaced total_preserved += preserved files_modified.append((file_path, replaced, preserved)) status = "[DRY RUN] Would modify" if args.dry_run else "Modified" print(f"{status}: {file_path} ({replaced} replaced, {preserved} preserved)") print(f"\n{'='*60}") print(f"Total files {'would be ' if args.dry_run else ''}modified: {len(files_modified)}") print(f"Total instances replaced: {total_replaced}") print(f"Total instances preserved: {total_preserved}") print(f"{'='*60}") if args.dry_run and len(files_modified) > 0: print("\nRun without --dry-run to apply changes") if __name__ == '__main__': main()