# 02 - Migration Wizard ## Overview A step-by-step wizard that guides users through the migration process. The wizard handles project copying, scanning, reporting, and executing migrations. ## Wizard Steps 1. **Confirm** - Confirm source/target paths 2. **Scan** - Analyze project for migration needs 3. **Report** - Show what needs to change 4. **Configure** - (Optional) Set up AI assistance 5. **Migrate** - Execute the migration 6. **Complete** - Summary and next steps ## State Machine ```typescript // packages/noodl-editor/src/editor/src/models/migration/MigrationSession.ts type MigrationStep = | 'confirm' | 'scanning' | 'report' | 'configureAi' | 'migrating' | 'complete' | 'failed'; interface MigrationSession { id: string; step: MigrationStep; // Source project source: { path: string; name: string; runtimeVersion: 'react17'; }; // Target (copy) project target: { path: string; copied: boolean; }; // Scan results scan?: { completedAt: string; totalComponents: number; totalNodes: number; customJsFiles: number; categories: { automatic: ComponentMigrationInfo[]; simpleFixes: ComponentMigrationInfo[]; needsReview: ComponentMigrationInfo[]; }; }; // AI configuration ai?: { enabled: boolean; apiKey?: string; // Only stored in memory during session budget: { max: number; spent: number; pauseIncrement: number; }; }; // Migration progress progress?: { phase: 'copying' | 'automatic' | 'ai-assisted' | 'finalizing'; current: number; total: number; currentComponent?: string; log: MigrationLogEntry[]; }; // Final result result?: { success: boolean; migrated: number; needsReview: number; failed: number; totalCost: number; duration: number; }; } interface ComponentMigrationInfo { id: string; name: string; path: string; issues: MigrationIssue[]; estimatedCost?: number; } interface MigrationIssue { id: string; type: MigrationIssueType; description: string; location: { file: string; line: number; column?: number; }; autoFixable: boolean; fix?: { type: 'automatic' | 'ai-required'; description: string; }; } type MigrationIssueType = | 'componentWillMount' | 'componentWillReceiveProps' | 'componentWillUpdate' | 'unsafeLifecycle' | 'stringRef' | 'legacyContext' | 'createFactory' | 'findDOMNode' | 'reactDomRender' | 'other'; interface MigrationLogEntry { timestamp: string; level: 'info' | 'success' | 'warning' | 'error'; component?: string; message: string; details?: string; cost?: number; } ``` ## Step 1: Confirm ```tsx // packages/noodl-editor/src/editor/src/views/migration/steps/ConfirmStep.tsx interface ConfirmStepProps { session: MigrationSession; onUpdateTarget: (path: string) => void; onNext: () => void; onCancel: () => void; } function ConfirmStep({ session, onUpdateTarget, onNext, onCancel }: ConfirmStepProps) { const [targetPath, setTargetPath] = useState(session.target.path); const [targetExists, setTargetExists] = useState(false); useEffect(() => { checkPathExists(targetPath).then(setTargetExists); }, [targetPath]); const handleTargetChange = (newPath: string) => { setTargetPath(newPath); onUpdateTarget(newPath); }; return (
} readonly />
Creates copy
} /> {targetExists && (
)}

What happens next:

  1. Your project will be copied to the new location
  2. We'll scan for compatibility issues
  3. You'll see a report of what needs to change
  4. Optionally, AI can help fix complex code
); } ``` ## Step 2: Scanning ```tsx // packages/noodl-editor/src/editor/src/views/migration/steps/ScanningStep.tsx interface ScanningStepProps { session: MigrationSession; onComplete: (scan: MigrationScan) => void; onError: (error: Error) => void; } function ScanningStep({ session, onComplete, onError }: ScanningStepProps) { const [phase, setPhase] = useState<'copying' | 'scanning'>('copying'); const [progress, setProgress] = useState(0); const [currentItem, setCurrentItem] = useState(''); const [stats, setStats] = useState({ components: 0, nodes: 0, jsFiles: 0 }); useEffect(() => { runScan(); }, []); const runScan = async () => { try { // Phase 1: Copy project setPhase('copying'); await copyProject(session.source.path, session.target.path, { onProgress: (p, item) => { setProgress(p * 50); // 0-50% setCurrentItem(item); } }); // Phase 2: Scan for issues setPhase('scanning'); const scan = await scanProject(session.target.path, { onProgress: (p, item, partialStats) => { setProgress(50 + p * 50); // 50-100% setCurrentItem(item); setStats(partialStats); } }); onComplete(scan); } catch (error) { onError(error); } }; return (
{currentItem && ( <> {currentItem} )}
{phase === 'scanning' && (
Looking for React 17 patterns that need updating...
)}
); } ``` ## Step 3: Report ```tsx // packages/noodl-editor/src/editor/src/views/migration/steps/ReportStep.tsx interface ReportStepProps { session: MigrationSession; onConfigureAi: () => void; onMigrateWithoutAi: () => void; onMigrateWithAi: () => void; onCancel: () => void; } function ReportStep({ session, onConfigureAi, onMigrateWithoutAi, onMigrateWithAi, onCancel }: ReportStepProps) { const { scan } = session; const [expandedCategory, setExpandedCategory] = useState(null); const totalIssues = scan.categories.simpleFixes.length + scan.categories.needsReview.length; const estimatedCost = scan.categories.simpleFixes .concat(scan.categories.needsReview) .reduce((sum, c) => sum + (c.estimatedCost || 0), 0); const allAutomatic = totalIssues === 0; return (
{/* Summary Stats */}
} value={scan.categories.automatic.length} label="Automatic" variant="success" /> } value={scan.categories.simpleFixes.length} label="Simple Fixes" variant="info" /> } value={scan.categories.needsReview.length} label="Needs Review" variant="warning" />
{/* Category Details */}
} items={scan.categories.automatic} variant="success" expanded={expandedCategory === 'automatic'} onToggle={() => setExpandedCategory( expandedCategory === 'automatic' ? null : 'automatic' )} /> {scan.categories.simpleFixes.length > 0 && ( } items={scan.categories.simpleFixes} variant="info" expanded={expandedCategory === 'simpleFixes'} onToggle={() => setExpandedCategory( expandedCategory === 'simpleFixes' ? null : 'simpleFixes' )} showIssueDetails /> )} {scan.categories.needsReview.length > 0 && ( } items={scan.categories.needsReview} variant="warning" expanded={expandedCategory === 'needsReview'} onToggle={() => setExpandedCategory( expandedCategory === 'needsReview' ? null : 'needsReview' )} showIssueDetails /> )}
{/* AI Assistance Prompt */} {!allAutomatic && (

AI-Assisted Migration Available

Claude can automatically fix the {totalIssues} components that need code changes. Estimated cost: ~${estimatedCost.toFixed(2)}

)}
{allAutomatic ? ( ) : ( <> {session.ai?.enabled && ( )} )}
); } // Category Section Component function CategorySection({ title, description, icon, items, variant, expanded, onToggle, showIssueDetails = false }: CategorySectionProps) { return (
{expanded && (
{items.map(item => (
{item.name} {showIssueDetails && item.issues.length > 0 && (
    {item.issues.map(issue => (
  • {issue.type} {issue.description}
  • ))}
)}
{item.estimatedCost && ( ~${item.estimatedCost.toFixed(2)} )}
))}
)}
); } ``` ## Step 4: Migration Progress ```tsx // packages/noodl-editor/src/editor/src/views/migration/steps/MigratingStep.tsx interface MigratingStepProps { session: MigrationSession; useAi: boolean; onPause: () => void; onAiDecision: (decision: AiDecision) => void; onComplete: (result: MigrationResult) => void; onError: (error: Error) => void; } interface AiDecision { componentId: string; action: 'retry' | 'skip' | 'manual' | 'getHelp'; } function MigratingStep({ session, useAi, onPause, onAiDecision, onComplete, onError }: MigratingStepProps) { const [awaitingDecision, setAwaitingDecision] = useState(null); const { progress, ai } = session; const budgetPercent = ai ? (ai.budget.spent / ai.budget.max) * 100 : 0; return (
{/* Budget Display (if using AI) */} {useAi && ai && (
Budget ${ai.budget.spent.toFixed(2)} / ${ai.budget.max.toFixed(2)}
80 ? 'warning' : 'default'} />
)} {/* Component Progress */}
{progress?.log.slice(-5).map((entry, i) => ( ))} {progress?.currentComponent && !awaitingDecision && (
{progress.currentComponent} {useAi && ~$0.08}
)}
{/* AI Decision Required */} {awaitingDecision && ( { setAwaitingDecision(null); onAiDecision(decision); }} /> )} {/* Overall Progress */}
{progress?.current || 0} / {progress?.total || 0} components
); } // Log Entry Component function LogEntry({ entry }: { entry: MigrationLogEntry }) { const icons = { info: , success: , warning: , error: }; return (
{icons[entry.level]}
{entry.component && ( {entry.component} )} {entry.message}
{entry.cost && ( ${entry.cost.toFixed(2)} )}
); } // AI Decision Panel function AiDecisionPanel({ request, budget, onDecision }: { request: AiDecisionRequest; budget: MigrationBudget; onDecision: (decision: AiDecision) => void; }) { return (

{request.componentName} - Needs Your Input

Claude attempted {request.attempts} migrations but the component still has issues. Here's what happened:

{request.attemptHistory.map((attempt, i) => (
Attempt {i + 1}: {attempt.description}
))}
Cost so far: ${request.costSpent.toFixed(2)}
); } ``` ## Step 5: Complete ```tsx // packages/noodl-editor/src/editor/src/views/migration/steps/CompleteStep.tsx interface CompleteStepProps { session: MigrationSession; onViewLog: () => void; onOpenProject: () => void; } function CompleteStep({ session, onViewLog, onOpenProject }: CompleteStepProps) { const { result, source, target } = session; const hasIssues = result.needsReview > 0; return ( : } >
{/* Summary */}
} value={result.migrated} label="Migrated" variant="success" /> {result.needsReview > 0 && ( } value={result.needsReview} label="Needs Review" variant="warning" /> )} {result.failed > 0 && ( } value={result.failed} label="Failed" variant="error" /> )}
{session.ai?.enabled && (
AI cost: ${result.totalCost.toFixed(2)}
)}
Time: {formatDuration(result.duration)}
{/* Project Paths */}

Project Locations

} /> } actions={[ { label: 'Show in Finder', onClick: () => showInFinder(target.path) } ]} />
{/* What's Next */}

What's Next?

    {result.needsReview > 0 && (
  1. Components marked with ⚠️ have notes in the component panel - click to see migration details
  2. )}
  3. Test your app thoroughly before deploying
  4. Once confirmed working, you can archive or delete the original folder
); } ``` ## Wizard Container ```tsx // packages/noodl-editor/src/editor/src/views/migration/MigrationWizard.tsx interface MigrationWizardProps { sourcePath: string; onComplete: (targetPath: string) => void; onCancel: () => void; } function MigrationWizard({ sourcePath, onComplete, onCancel }: MigrationWizardProps) { const [session, dispatch] = useReducer(migrationReducer, { id: generateId(), step: 'confirm', source: { path: sourcePath, name: path.basename(sourcePath), runtimeVersion: 'react17' }, target: { path: `${sourcePath}-r19`, copied: false } }); const renderStep = () => { switch (session.step) { case 'confirm': return ( dispatch({ type: 'SET_TARGET_PATH', path })} onNext={() => dispatch({ type: 'START_SCAN' })} onCancel={onCancel} /> ); case 'scanning': return ( dispatch({ type: 'SCAN_COMPLETE', scan })} onError={(error) => dispatch({ type: 'ERROR', error })} /> ); case 'report': return ( dispatch({ type: 'CONFIGURE_AI' })} onMigrateWithoutAi={() => dispatch({ type: 'START_MIGRATE', useAi: false })} onMigrateWithAi={() => dispatch({ type: 'START_MIGRATE', useAi: true })} onCancel={onCancel} /> ); case 'configureAi': return ( dispatch({ type: 'SAVE_AI_CONFIG', config })} onBack={() => dispatch({ type: 'BACK_TO_REPORT' })} /> ); case 'migrating': return ( dispatch({ type: 'PAUSE' })} onAiDecision={(d) => dispatch({ type: 'AI_DECISION', decision: d })} onComplete={(result) => dispatch({ type: 'COMPLETE', result })} onError={(error) => dispatch({ type: 'ERROR', error })} /> ); case 'complete': return ( openMigrationLog(session)} onOpenProject={() => onComplete(session.target.path)} /> ); case 'failed': return ( dispatch({ type: 'RETRY' })} onCancel={onCancel} /> ); } }; return ( {renderStep()} ); } ``` ## Testing Checklist - [ ] Wizard opens from project detection - [ ] Target path can be customized - [ ] Duplicate path detection works - [ ] Scanning shows progress - [ ] Report categorizes components correctly - [ ] AI config button appears when needed - [ ] Migration progress updates in real-time - [ ] AI decision panel appears on failure - [ ] Complete screen shows correct stats - [ ] "Open Project" launches migrated project - [ ] Cancel works at every step - [ ] Errors are handled gracefully