Files
pm-template/web/server.js

78 lines
2.1 KiB
JavaScript
Raw Normal View History

const express = require('express');
const { WebSocketServer } = require('ws');
const { spawn } = require('child_process');
const path = require('path');
const http = require('http');
const PORT = process.env.PORT || 3000;
const app = express();
const server = http.createServer(app);
const wss = new WebSocketServer({ server, path: '/ws' });
app.use(express.static(path.join(__dirname, 'public')));
app.get('/health', (_, res) => res.json({ status: 'ok' }));
wss.on('connection', (ws) => {
console.log('[forge] client connected');
// Spawn codex in interactive mode, working dir = repo root
const repoRoot = path.resolve(__dirname, '..');
const codex = spawn('codex', ['--quiet'], {
cwd: repoRoot,
shell: true,
env: { ...process.env, FORCE_COLOR: '0' },
stdio: ['pipe', 'pipe', 'pipe']
});
let alive = true;
codex.stdout.on('data', (data) => {
if (ws.readyState === 1) {
ws.send(JSON.stringify({ type: 'stdout', data: data.toString() }));
}
});
codex.stderr.on('data', (data) => {
if (ws.readyState === 1) {
ws.send(JSON.stringify({ type: 'stderr', data: data.toString() }));
}
});
codex.on('close', (code) => {
alive = false;
console.log(`[forge] codex exited (code ${code})`);
if (ws.readyState === 1) {
ws.send(JSON.stringify({ type: 'exit', code }));
}
});
codex.on('error', (err) => {
alive = false;
console.error('[forge] codex spawn error:', err.message);
if (ws.readyState === 1) {
ws.send(JSON.stringify({ type: 'error', data: err.message }));
}
});
ws.on('message', (msg) => {
if (alive && codex.stdin.writable) {
codex.stdin.write(msg.toString() + '\n');
}
});
ws.on('close', () => {
console.log('[forge] client disconnected');
if (alive) codex.kill('SIGTERM');
});
});
server.listen(PORT, () => {
console.log(`\n 🔥 Forge Console running at http://localhost:${PORT}\n`);
});
process.on('SIGINT', () => {
console.log('\n[forge] shutting down...');
wss.clients.forEach((ws) => ws.close());
server.close(() => process.exit(0));
});