- web/: Local chat UI (Express + WS → Codex bridge) - openwebui/: Preset, pipelines, knowledge manifest - Dockerfile + docker-compose.yml - Updated README with 3 frontend options - CLI-agnostic: works with Codex, Claude Code, Kiro, Gemini
78 lines
2.1 KiB
JavaScript
78 lines
2.1 KiB
JavaScript
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));
|
|
});
|