UNPKG

v0-ui-reviewer

Version:

Next-gen UI/UX reviewer with multi-model AI support (OpenAI, Claude, v0), style extraction, and live preview sandbox

215 lines (214 loc) • 7.76 kB
import { spawn } from 'child_process'; import { promises as fs } from 'fs'; import path from 'path'; import chalk from 'chalk'; import ora from 'ora'; import boxen from 'boxen'; import open from 'open'; import { io } from 'socket.io-client'; export class SandboxLauncher { nextProcess = null; wsProcess = null; socket = null; sandboxPath; constructor() { this.sandboxPath = path.join(process.cwd(), 'sandbox'); } async launch() { const spinner = ora('Checking sandbox environment...').start(); try { // Check if sandbox exists const sandboxExists = await this.checkSandboxExists(); if (!sandboxExists) { spinner.fail('Sandbox not found'); console.log(chalk.yellow('\nThe sandbox directory was not found.')); console.log(chalk.cyan('Please ensure the sandbox is set up in your project.')); process.exit(1); } // Check if dependencies are installed spinner.text = 'Checking dependencies...'; const depsInstalled = await this.checkDependencies(); if (!depsInstalled) { spinner.text = 'Installing dependencies...'; await this.installDependencies(); } spinner.succeed('Sandbox environment ready'); // Display sandbox info console.log(boxen(`šŸš€ ${chalk.bold('V0 UI Reviewer Sandbox')}\n\n` + `šŸ“± Preview URL: ${chalk.cyan('http://localhost:3001')}\n` + `šŸ”Œ WebSocket: ${chalk.cyan('ws://localhost:3002')}\n` + `šŸ“ Location: ${chalk.gray(this.sandboxPath)}\n\n` + `${chalk.yellow('Features:')}\n` + `• Side-by-side design comparison\n` + `• Live style updates via WebSocket\n` + `• Hot module reload\n` + `• Pre-built UI components`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'cyan' })); // Start the sandbox console.log(chalk.cyan('\nšŸš€ Starting sandbox servers...\n')); // Start WebSocket server first await this.startWebSocketServer(); // Wait a bit for WebSocket to be ready await new Promise(resolve => setTimeout(resolve, 1000)); // Start Next.js dev server await this.startNextServer(); // Open browser after a short delay setTimeout(() => { open('http://localhost:3001'); }, 3000); // Set up cleanup handlers this.setupCleanupHandlers(); // Keep the process running console.log(chalk.green('\nāœ… Sandbox is running!')); console.log(chalk.gray('Press Ctrl+C to stop\n')); } catch (error) { spinner.fail('Failed to launch sandbox'); console.error(chalk.red('Error:'), error); this.cleanup(); process.exit(1); } } async checkSandboxExists() { try { await fs.access(this.sandboxPath); return true; } catch { return false; } } async checkDependencies() { try { const nodeModulesPath = path.join(this.sandboxPath, 'node_modules'); await fs.access(nodeModulesPath); return true; } catch { return false; } } async installDependencies() { return new Promise((resolve, reject) => { const install = spawn('npm', ['install'], { cwd: this.sandboxPath, stdio: 'inherit', shell: true }); install.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`npm install failed with code ${code}`)); } }); install.on('error', reject); }); } async startWebSocketServer() { return new Promise((resolve, reject) => { this.wsProcess = spawn('npm', ['run', 'ws'], { cwd: this.sandboxPath, stdio: 'pipe', shell: true }); this.wsProcess.stdout?.on('data', (data) => { const output = data.toString(); if (output.includes('WebSocket server running')) { console.log(chalk.green('āœ“ WebSocket server started on port 3002')); resolve(); } }); this.wsProcess.stderr?.on('data', (data) => { console.error(chalk.red('WebSocket Error:'), data.toString()); }); this.wsProcess.on('error', (error) => { console.error(chalk.red('Failed to start WebSocket server:'), error); reject(error); }); // Timeout after 10 seconds setTimeout(() => { reject(new Error('WebSocket server failed to start in time')); }, 10000); }); } async startNextServer() { return new Promise((resolve, reject) => { this.nextProcess = spawn('npm', ['run', 'dev'], { cwd: this.sandboxPath, stdio: 'pipe', shell: true }); let resolved = false; this.nextProcess.stdout?.on('data', (data) => { const output = data.toString(); process.stdout.write(output); if (!resolved && output.includes('Ready')) { resolved = true; console.log(chalk.green('\nāœ“ Next.js server started on http://localhost:3001')); resolve(); } }); this.nextProcess.stderr?.on('data', (data) => { process.stderr.write(data); }); this.nextProcess.on('error', (error) => { console.error(chalk.red('Failed to start Next.js server:'), error); reject(error); }); // Timeout after 30 seconds setTimeout(() => { if (!resolved) { reject(new Error('Next.js server failed to start in time')); } }, 30000); }); } setupCleanupHandlers() { const cleanup = () => { console.log(chalk.yellow('\n\nšŸ›‘ Shutting down sandbox...')); this.cleanup(); process.exit(0); }; process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); } cleanup() { if (this.nextProcess) { this.nextProcess.kill('SIGTERM'); this.nextProcess = null; } if (this.wsProcess) { this.wsProcess.kill('SIGTERM'); this.wsProcess = null; } if (this.socket) { this.socket.disconnect(); this.socket = null; } } // Method to send style updates to the sandbox async sendStyleUpdate(selector, styles, component) { if (!this.socket) { this.socket = io('http://localhost:3002'); } this.socket.emit('style-update', { selector, styles, component, timestamp: Date.now() }); } // Method to reset all styles async resetStyles() { if (!this.socket) { this.socket = io('http://localhost:3002'); } this.socket.emit('style-reset'); } }