'use client'; import React, { useState, useCallback, useRef } from 'react'; import { Cloud, Package, Container, Download, Rocket, ArrowLeft, } from 'lucide-react'; import { cn } from '@/lib/utils'; import type { DeployTarget, DeployResult, } from '@mcpengine/ai-pipeline/types'; import { DeployStepIndicator, type StepState, } from './DeployStepIndicator'; import { DeployLogViewer, type LogEntry } from './DeployLogViewer'; import { DeploySuccess } from './DeploySuccess'; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- type DeployPhase = 'select' | 'deploying' | 'success' | 'error'; type DeployStep = 'build' | 'test' | 'package' | 'deploy' | 'verify'; interface StepConfig { key: DeployStep; label: string; } const STEPS: StepConfig[] = [ { key: 'build', label: 'Build' }, { key: 'test', label: 'Test' }, { key: 'package', label: 'Package' }, { key: 'deploy', label: 'Deploy' }, { key: 'verify', label: 'Verify' }, ]; interface TargetOption { id: DeployTarget; label: string; description: string; icon: React.ReactNode; available: boolean; badge?: string; } // --------------------------------------------------------------------------- // Props // --------------------------------------------------------------------------- export interface DeployPageProps { projectId: string; projectName?: string; projectSlug?: string; className?: string; } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export function DeployPage({ projectId, projectName = 'MCP Server', projectSlug, className, }: DeployPageProps) { const [phase, setPhase] = useState('select'); const [selectedTarget, setSelectedTarget] = useState(null); const [stepStates, setStepStates] = useState>({ build: 'waiting', test: 'waiting', package: 'waiting', deploy: 'waiting', verify: 'waiting', }); const [logs, setLogs] = useState([]); const [result, setResult] = useState(null); const [error, setError] = useState(null); const abortRef = useRef(null); // ── Target options ───────────────────────────────────────────────────── const targets: TargetOption[] = [ { id: 'mcpengine', label: 'MCPEngine', description: 'Deploy to MCPEngine cloud. Instant, zero-config.', icon: , available: true, badge: 'Recommended', }, { id: 'npm', label: 'npm', description: 'Publish as an npm package for local installation.', icon: , available: false, badge: 'Coming Soon', }, { id: 'docker', label: 'Docker', description: 'Build a Docker image for self-hosted deployment.', icon: , available: false, badge: 'Coming Soon', }, { id: 'download', label: 'Download', description: 'Download source code with all config files.', icon: , available: true, }, ]; // ── Append log ───────────────────────────────────────────────────────── const addLog = useCallback( (message: string, level: LogEntry['level'] = 'info') => { setLogs((prev) => [ ...prev, { timestamp: new Date().toISOString(), message, level }, ]); }, [], ); // ── Start deploy ─────────────────────────────────────────────────────── const startDeploy = useCallback( async (target: DeployTarget) => { setSelectedTarget(target); setPhase('deploying'); setLogs([]); setError(null); setResult(null); setStepStates({ build: 'active', test: 'waiting', package: 'waiting', deploy: 'waiting', verify: 'waiting', }); addLog(`Starting deployment to ${target}…`, 'info'); const controller = new AbortController(); abortRef.current = controller; try { const res = await fetch('/api/deploy', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ projectId, target, config: { slug: projectSlug ?? projectId.slice(0, 12), }, }), signal: controller.signal, }); if (!res.ok) { const errData = await res.json().catch(() => ({})); throw new Error(errData.error ?? `HTTP ${res.status}`); } // Read SSE stream const reader = res.body?.getReader(); if (!reader) throw new Error('No response body'); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n\n'); buffer = lines.pop() ?? ''; for (const line of lines) { if (!line.startsWith('data: ')) continue; try { const event = JSON.parse(line.slice(6)); handleEvent(event); } catch { // Skip malformed events } } } } catch (err: any) { if (err.name === 'AbortError') return; const msg = err.message ?? 'Deployment failed'; addLog(msg, 'error'); setError(msg); setPhase('error'); // Mark current active step as failed setStepStates((prev) => { const next = { ...prev }; for (const step of STEPS) { if (next[step.key] === 'active') { next[step.key] = 'failed'; } } return next; }); } }, [projectId, projectSlug, addLog], ); // ── Handle SSE event ─────────────────────────────────────────────────── const handleEvent = useCallback( (event: any) => { const { type, step, message, level, result: eventResult } = event; // Add to log if (message) { addLog(message, level ?? 'info'); } // Update step states if (step && type === 'progress') { setStepStates((prev) => { const next = { ...prev }; // Complete all steps before current const stepIdx = STEPS.findIndex((s) => s.key === step); for (let i = 0; i < stepIdx; i++) { if (next[STEPS[i].key] !== 'failed') { next[STEPS[i].key] = 'complete'; } } // Set current step based on level if (level === 'success') { next[step as DeployStep] = 'complete'; // Activate next step if (stepIdx + 1 < STEPS.length) { next[STEPS[stepIdx + 1].key] = 'active'; } } else if (level === 'error') { next[step as DeployStep] = 'failed'; } else { next[step as DeployStep] = 'active'; } return next; }); } // Deploy complete if (type === 'complete' && eventResult) { setResult(eventResult); setStepStates({ build: 'complete', test: 'complete', package: 'complete', deploy: 'complete', verify: 'complete', }); setTimeout(() => setPhase('success'), 500); } // Error if (type === 'error') { setError(message); setPhase('error'); } }, [addLog], ); // ── Reset ────────────────────────────────────────────────────────────── const reset = () => { abortRef.current?.abort(); setPhase('select'); setSelectedTarget(null); setLogs([]); setError(null); setResult(null); setStepStates({ build: 'waiting', test: 'waiting', package: 'waiting', deploy: 'waiting', verify: 'waiting', }); }; // ── Render: Target selection ─────────────────────────────────────────── if (phase === 'select') { return (

Deploy {projectName}

Choose a deployment target for your MCP server

{targets.map((t) => ( ))}
); } // ── Render: Deploying / Error ────────────────────────────────────────── if (phase === 'deploying' || phase === 'error') { return (
{/* Back button */} {/* Header */}

Deploying to{' '} {selectedTarget}

{projectName}

{/* Step indicator */}
{STEPS.map((step, i) => ( ))}
{/* Error banner */} {phase === 'error' && error && (
Error: {error}
)} {/* Log viewer */} {/* Retry on error */} {phase === 'error' && selectedTarget && (
)}
); } // ── Render: Success ──────────────────────────────────────────────────── if (phase === 'success' && result) { return (
{ window.location.href = `/projects/${projectId}`; }} onViewServer={() => { if (result.url) window.open(result.url, '_blank'); }} onDeployAnother={reset} />
); } return null; }