UNPKG

@masuidrive/ticket

Version:

Real-time ticket tracking viewer with Vite + Express

320 lines (317 loc) 12.6 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UnifiedServerSSE = void 0; const http_1 = require("http"); const path_1 = __importDefault(require("path")); const express_1 = __importDefault(require("express")); const cors_1 = __importDefault(require("cors")); const fileService_1 = require("./src/fileService"); const fileWatcher_1 = require("./src/fileWatcher"); // IMPORTANT: Port 4932 is REQUIRED for this project - DO NOT CHANGE // This port was explicitly specified by the project requirements // Changing this port will break the application functionality const DEFAULT_PORT = 4932; const dev = process.env.NODE_ENV !== 'production'; const hostname = 'localhost'; const port = parseInt(process.env.PORT || DEFAULT_PORT.toString(), 10); // Vite build directory const distPath = path_1.default.join(process.cwd(), 'dist'); class UnifiedServerSSE { constructor(projectRoot) { this.sseClients = new Map(); this.clientIdCounter = 0; this.projectRoot = projectRoot || process.cwd(); this.expressApp = (0, express_1.default)(); this.fileService = new fileService_1.FileService(this.projectRoot); this.fileWatcher = new fileWatcher_1.FileWatcher(this.projectRoot); this.setupMiddleware(); this.setupExpressRoutes(); this.setupFileWatcher(); } setupMiddleware() { this.expressApp.use((0, cors_1.default)()); this.expressApp.use(express_1.default.json()); // Serve static files from Vite build directory this.expressApp.use(express_1.default.static(distPath)); } setupExpressRoutes() { // Express API routes this.expressApp.get('/api/express/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); this.expressApp.get('/api/express/ticket', async (req, res) => { try { const content = await this.fileService.readTicketFile(); if (!content) { return res.status(404).json({ error: 'Ticket file not found', message: 'No current-ticket.md file exists in the project root' }); } res.json(content); } catch (error) { console.error('Error reading ticket file:', error); res.status(500).json({ error: 'Internal server error', message: 'Failed to read ticket file' }); } }); this.expressApp.get('/api/express/ticket/exists', async (req, res) => { try { const exists = await this.fileService.fileExists('current-ticket.md'); res.json({ exists }); } catch (error) { console.error('Error checking file existence:', error); res.status(500).json({ error: 'Internal server error', message: 'Failed to check file existence' }); } }); this.expressApp.get('/api/system-info', async (req, res) => { try { const { exec } = require('child_process'); const { promisify } = require('util'); const execAsync = promisify(exec); // Get current directory const currentDir = this.projectRoot; // Get git branch let gitBranch = ''; try { const { stdout } = await execAsync('git branch --show-current', { cwd: currentDir }); gitBranch = stdout.trim(); } catch (gitError) { // Git command failed, branch might not exist or not a git repo gitBranch = ''; } res.json({ currentDir, gitBranch }); } catch (error) { console.error('Error getting system info:', error); res.status(500).json({ error: 'Internal server error', message: 'Failed to get system info' }); } }); // SSE endpoint this.expressApp.get('/api/ticket-stream', async (req, res) => { // Set SSE headers res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Accel-Buffering', 'no'); // Disable Nginx buffering // Generate unique client ID const clientId = `client-${++this.clientIdCounter}`; console.log(`SSE client connected: ${clientId}`); // Store client this.sseClients.set(clientId, { id: clientId, res }); // Send initial update notification if file exists try { const exists = await this.fileService.fileExists('current-ticket.md'); if (exists) { this.sendSSEMessage(res, 'update', { ticket: 'current-ticket.md' }); } } catch (error) { console.error('Error sending initial notification:', error); } // Send periodic heartbeat to keep connection alive const heartbeatInterval = setInterval(() => { this.sendSSEMessage(res, 'heartbeat', { timestamp: Date.now() }); }, 30000); // Every 30 seconds // Handle client disconnect req.on('close', () => { console.log(`SSE client disconnected: ${clientId}`); clearInterval(heartbeatInterval); this.sseClients.delete(clientId); }); }); } setupFileWatcher() { this.fileWatcher.watchFile('current-ticket.md'); this.fileWatcher.on('fileChanged', async () => { console.log('Ticket file changed'); await this.broadcastUpdate(); }); this.fileWatcher.on('fileAdded', async () => { console.log('Ticket file added'); await this.broadcastUpdate(); }); this.fileWatcher.on('fileRemoved', () => { console.log('Ticket file removed'); this.broadcast('removed', { ticket: 'current-ticket.md' }); }); this.fileWatcher.on('error', (error) => { console.error('File watcher error:', error); }); } sendSSEMessage(res, eventType, data) { const message = `event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`; res.write(message); } async broadcastUpdate() { try { // Send only update notification, not the content this.broadcast('update', { ticket: 'current-ticket.md' }); } catch (error) { console.error('Error broadcasting update:', error); } } broadcast(eventType, data) { this.sseClients.forEach((client) => { try { this.sendSSEMessage(client.res, eventType, data); } catch (error) { console.error(`Error sending to client ${client.id}:`, error); this.sseClients.delete(client.id); } }); } async start() { // For production, check if dist directory exists if (!dev) { const fs = await Promise.resolve().then(() => __importStar(require('fs'))); if (!fs.existsSync(distPath)) { throw new Error(`Production build directory not found at ${distPath}. Please run 'npm run build' first.`); } } // Serve index.html for SPA routes (fallback) this.expressApp.use((req, res, next) => { // Skip API routes if (req.path.startsWith('/api/')) { return res.status(404).json({ error: 'API endpoint not found' }); } // Skip files with extensions (likely static assets) if (path_1.default.extname(req.path)) { return res.status(404).send('File not found'); } // Serve index.html for all other routes (SPA fallback) res.sendFile(path_1.default.join(distPath, 'index.html'), (err) => { if (err) { console.error('Error serving index.html:', err); res.status(500).send('Error loading application'); } }); }); this.server = (0, http_1.createServer)(this.expressApp); this.server.listen(port, () => { console.log(`> Ready on http://${hostname}:${port}`); console.log(`> Express API available at: - GET /api/express/health - GET /api/express/ticket - GET /api/express/ticket/exists - GET /api/system-info - SSE /api/ticket-stream`); }); } stop() { this.fileWatcher.stop(); // Close all SSE connections this.sseClients.forEach((client) => { client.res.end(); }); this.sseClients.clear(); this.server.close(); } } exports.UnifiedServerSSE = UnifiedServerSSE; // Parse command line arguments function parseArgs() { const args = process.argv.slice(2); let projectRoot = process.cwd(); for (let i = 0; i < args.length; i++) { if ((args[i] === '-d' || args[i] === '--dir') && args[i + 1]) { projectRoot = args[i + 1]; i++; } else if (args[i] === '-h' || args[i] === '--help') { showHelp(); process.exit(0); } } return { projectRoot }; } function showHelp() { console.log(` Unified Ticket Viewer Server with SSE - Next.js + Express Usage: tsx server/unified-server-sse.ts [options] Options: -d, --dir <path> Project directory containing current-ticket.md -h, --help Show this help message Environment Variables: PORT Server port (default: ${DEFAULT_PORT}) NODE_ENV Set to 'production' for production mode `); } async function main() { const { projectRoot } = parseArgs(); console.log(`Starting Unified Server with SSE...`); console.log(`Project root: ${projectRoot}`); console.log(`Port: ${port}`); console.log(`Mode: ${dev ? 'development' : 'production'}`); const server = new UnifiedServerSSE(projectRoot); await server.start(); // Handle graceful shutdown process.on('SIGINT', () => { console.log('\nShutting down server...'); server.stop(); process.exit(0); }); process.on('SIGTERM', () => { console.log('\nShutting down server...'); server.stop(); process.exit(0); }); } // Run if this is the main module if (require.main === module) { main().catch(console.error); } //# sourceMappingURL=unified-server.js.map