UNPKG

crevr

Version:

A web-based UI for reviewing and reverting Claude Code changes

170 lines 6.68 kB
import express from 'express'; import { WebSocketServer } from 'ws'; import * as path from 'path'; import * as http from 'http'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); import { ClaudeLogParser } from './parser.js'; import { ChangeTracker } from './change-tracker.js'; export class RevertServer { constructor(port = 3456) { this.changes = []; this.port = port; this.app = express(); this.server = http.createServer(this.app); this.wss = new WebSocketServer({ server: this.server }); this.parser = new ClaudeLogParser(); this.tracker = new ChangeTracker(); this.setupRoutes(); this.setupWebSocket(); } setupRoutes() { // Serve static files from public directory this.app.use(express.static(path.join(__dirname, '..', 'public'))); // Serve index.html for root route this.app.get('/', (req, res) => { res.sendFile(path.join(__dirname, '..', 'public', 'index.html')); }); // API endpoint to get file content this.app.get('/api/file-content', async (req, res) => { try { const filePath = req.query.path; if (!filePath) { return res.status(400).json({ error: 'File path is required' }); } // Security check: ensure file is within current working directory const absolutePath = path.resolve(filePath); const cwd = process.cwd(); if (!absolutePath.startsWith(cwd)) { return res.status(403).json({ error: 'Access denied' }); } const fs = await import('fs'); const content = await fs.promises.readFile(absolutePath, 'utf-8'); res.send(content); } catch (error) { console.error('Error reading file:', error); res.status(404).json({ error: 'File not found' }); } }); // API endpoint to check file existence this.app.get('/api/file-exists', async (req, res) => { try { const filePath = req.query.path; if (!filePath) { return res.status(400).json({ error: 'File path is required' }); } // Security check: ensure file is within current working directory const absolutePath = path.resolve(filePath); const cwd = process.cwd(); if (!absolutePath.startsWith(cwd)) { return res.status(403).json({ error: 'Access denied' }); } const fs = await import('fs'); try { await fs.promises.access(absolutePath); res.json({ exists: true }); } catch { res.json({ exists: false }); } } catch (error) { res.status(500).json({ error: 'Failed to check file existence' }); } }); } setupWebSocket() { this.wss.on('connection', (ws) => { console.log('Client connected'); ws.on('message', async (message) => { try { const data = JSON.parse(message.toString()); switch (data.type) { case 'getChanges': await this.handleGetChanges(ws); break; case 'revert': await this.handleRevert(ws, data.changeId); break; } } catch (error) { console.error('WebSocket message error:', error); ws.send(JSON.stringify({ type: 'error', error: 'Invalid message format' })); } }); ws.on('close', () => { console.log('Client disconnected'); }); }); } async handleGetChanges(ws) { try { // Get and parse changes from Claude logs console.log('Getting file changes...'); const fileChanges = await this.parser.getFileChanges(); console.log(`Found ${fileChanges.length} file changes`); console.log('Processing changes...'); this.changes = await this.tracker.processChanges(fileChanges); console.log(`Processed ${this.changes.length} changes`); // Sort changes by timestamp (newest first) this.changes.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); console.log('Sending changes to client:', this.changes.length); ws.send(JSON.stringify({ type: 'changes', changes: this.changes })); } catch (error) { console.error('Error getting changes:', error); ws.send(JSON.stringify({ type: 'error', error: error.message || 'Failed to load changes' })); } } async handleRevert(ws, changeId) { try { console.log('Handling revert for change:', changeId); const change = this.changes.find(c => c.id === changeId); if (!change) { console.error('Change not found:', changeId); throw new Error('Change not found'); } console.log('Found change:', change); await this.tracker.revertChange(change); console.log('Revert successful'); ws.send(JSON.stringify({ type: 'revertSuccess', changeId })); } catch (error) { console.error('Error reverting change:', error); ws.send(JSON.stringify({ type: 'revertError', error: error.message || 'Failed to revert change' })); } } async start() { // Initialize the change tracker await this.tracker.init(); return new Promise((resolve) => { this.server.listen(this.port, () => { console.log(`Claude Revert server running on http://localhost:${this.port}`); resolve(true); }); }); } stop() { this.server.close(); this.wss.close(); } } //# sourceMappingURL=server.js.map