← back to blog
developer productivity

How to Run a 155-File Migration With AI Agents Without Losing Your Mind

6 min read

We migrated xeve's web dashboard from hardcoded hex colors to theme-aware CSS custom properties. 155 files, 1,874 color references, done in a single session with Claude Code. The result works. The process was messy. Here is the playbook we would use next time.

Phase Your Migration

The single most important decision was breaking the work into phases with build verification between each one:

  1. Phase 0: CSS foundation — define custom properties, update @theme, modify CSS classes. Two files. Build and verify.
  2. Phase 1: Infrastructure — ThemeProvider, ThemeToggle, layout.tsx changes. Four files. Build and verify.
  3. Phase 2: Bulk Tailwind classes — sed script across all TSX files. 130+ files. Build and verify.
  4. Phase 3: Chart components — parallel agents for Recharts JS color props. 18 files. Build and verify.
  5. Phase 4: Special cases — manual fixes for edge cases. 10 files. Build and verify.

Each phase has a clear scope and a build gate. When Phase 2 introduced a type error, we caught it before Phase 3 started. When a Phase 3 agent made an inconsistent rename, the build after Phase 3 caught it immediately.

The rule: never start a new phase until the current phase builds clean.

sed for the Boring Parts, Agents for the Interesting Parts

Phase 2 was pure find-and-replace: bg-[#151517] becomes bg-surface, text-[#e5e5e5] becomes text-text-primary. This is not a job for an AI — it is a job for sed. We wrote a shell script with 40+ sed commands and ran it in one pass across all TSX files.

The script was not perfect. It missed some patterns (placeholder-[#444], divide-[#1a1a1a]) that we caught with a post-migration grep. But it handled 90% of the work instantly and deterministically. No hallucination risk, no inconsistency between files, no agent coordination needed.

Phase 3 was the opposite — Recharts components use JavaScript color props (fill: '#ddd', stroke: '#333'), and each file has a unique structure. This is where agents shine. We spawned three parallel agents, each handling 6 chart files, each instructed to:

  1. Import useThemeColors()
  2. Add const colors = useThemeColors() at the top of the component
  3. Replace hardcoded theme colors with colors.textSecondary, colors.border, etc.
  4. Leave semantic colors (data visualization greens, reds, blues) untouched

The rule: use deterministic tools (sed, regex) for mechanical changes. Use AI agents for changes that require understanding context.

The Semantic Color Trap

The hardest part of the migration was not the volume — it was distinguishing between colors that should theme and colors that should not. #333 appears in two completely different contexts:

  • As a border color (border-[#333]) — this should become border-border-subtle
  • As a chart grid stroke (stroke: '#333') — this should become colors.borderSubtle
  • As a data visualization "system" category — this should stay as #333

We solved this by giving agents an explicit exclusion list: "Do NOT touch category/semantic colors like green (#22c55e), red (#ef4444), blue (#3b82f6), yellow (#e8d44d), purple (#a855f7)." This worked well. The agents left all data-viz colors untouched.

The one mistake was text-[#333]. The sed script converted it to text-border-subtle — technically correct (it maps to the border-subtle token), but semantically wrong. Text should use text tokens. We caught this with a post-migration grep for text-border and changed them all to text-text-muted.

The rule: after a bulk migration, grep for semantic mismatches. A color token used in the wrong context (border color for text, text color for background) is a bug that compiles but looks wrong in one theme.

Server Components Cannot Use Hooks

Our chart components are all 'use client', so importing useThemeColors() works. But some dashboard pages are server components that have inline style={{ color: '#ccc' }} in their JSX. You cannot use a React hook in a server component.

The solution for server components: use CSS variable references directly in style objects:

// Client component (hook)
const colors = useThemeColors()
style={{ color: colors.textTertiary }}

// Server component (CSS var)
style={{ color: 'var(--theme-text-tertiary)' }}

We told agents to check for 'use client' at the top of each file and use the appropriate pattern. Most got it right. One agent added a hook import to a server component, which would have failed at runtime (not at build time — Next.js only catches this when the page is actually rendered).

The rule: when giving agents instructions that depend on file context (client vs server component), make the check explicit. "Read the file first. If it starts with 'use client', use the hook. Otherwise, use CSS variables."

Post-Migration Audit Checklist

After the full migration, we ran these checks:

  1. Remaining hardcoded colors: grep -rn 'bg-[#' --include="*.tsx" — found 3 remaining (all semantic data colors, intentionally kept)
  2. Remaining inline style colors: grep -rn "color: '#" --include="*.tsx" — found 1 remaining (a category color constant, intentionally kept)
  3. Semantic mismatches: grep -rn 'text-border' --include="*.tsx" — found 9 misuses, fixed all
  4. Missing hover variants: grep -rn 'hover:text-[#' --include="*.tsx" — found 0 (all caught by sed)
  5. Build: next build — clean compilation, no type errors
  6. Placeholder colors: grep -rn 'placeholder.*[#' --include="*.tsx" — found 6, fixed

Total time from start to clean build with both themes working: about 2 hours. Total time if done manually without AI: probably 2-3 days. The AI handled the volume. The human handled the judgment calls.

Would We Do It Again?

Yes, with two changes:

  1. Write the sed script first, run it, build, then deploy agents. We interleaved sed and agent work, which made debugging harder. Sequential phases with clear boundaries work better than parallel everything.
  2. Give agents a shared exclusion list as a constant, not prose. Instead of "do not touch green, red, blue..." in natural language, create a JSON file of excluded hex values and tell agents to reference it. Removes ambiguity about which exact hex codes are semantic.

Large-scale migrations are where AI coding assistants earn their keep. The boring 80% is automated. The interesting 20% gets your full attention. And the build command is your constant companion.

Written by Kevin — builder of xeve

Track your apps, coding, music, and health — all in one place.

try xeve free