|
|
import express from 'express'; |
|
|
import cors from 'cors'; |
|
|
import { config } from 'dotenv'; |
|
|
import { spawn } from 'child_process'; |
|
|
import { WebSocketServer } from 'ws'; |
|
|
import http from 'http'; |
|
|
|
|
|
|
|
|
config(); |
|
|
|
|
|
const app = express(); |
|
|
const PORT = process.env.PORT || 7860; |
|
|
|
|
|
|
|
|
app.use(cors()); |
|
|
app.use(express.json()); |
|
|
app.use(express.static('public')); |
|
|
|
|
|
|
|
|
let opencodeProcess = null; |
|
|
let opencodeStdin = null; |
|
|
let opencodeStdout = null; |
|
|
let opencodeStderr = null; |
|
|
|
|
|
|
|
|
function startOpenCode() { |
|
|
console.log('🚀 Starting OpenCode CLI TUI...'); |
|
|
|
|
|
const args = ['--no-color']; |
|
|
|
|
|
opencodeProcess = spawn('opencode', args, { |
|
|
stdio: ['pipe', 'pipe', 'pipe'], |
|
|
env: { |
|
|
...process.env, |
|
|
OPENCODE_DISABLE_AUTOUPDATE: 'true', |
|
|
OPENCODE_CLIENT: 'hf-tui-web', |
|
|
TERM: 'xterm-256color', |
|
|
COLORTERM: 'truecolor' |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
opencodeStdin = opencodeProcess.stdin; |
|
|
opencodeStdout = opencodeProcess.stdout; |
|
|
opencodeStderr = opencodeProcess.stderr; |
|
|
|
|
|
|
|
|
opencodeStdout.on('data', (data) => { |
|
|
broadcastToTerminals(data.toString()); |
|
|
}); |
|
|
|
|
|
opencodeStderr.on('data', (data) => { |
|
|
broadcastToTerminals(data.toString()); |
|
|
}); |
|
|
|
|
|
|
|
|
opencodeProcess.on('close', (code) => { |
|
|
console.log(`OpenCode process exited with code ${code}`); |
|
|
opencodeProcess = null; |
|
|
}); |
|
|
|
|
|
opencodeProcess.on('error', (error) => { |
|
|
console.error('OpenCode process error:', error); |
|
|
opencodeProcess = null; |
|
|
}); |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
if (opencodeStdout) { |
|
|
opencodeStdin.write('\n'); |
|
|
} |
|
|
}, 2000); |
|
|
|
|
|
return opencodeProcess; |
|
|
} |
|
|
|
|
|
|
|
|
const terminals = new Set(); |
|
|
|
|
|
function broadcastToTerminals(data) { |
|
|
terminals.forEach((ws) => { |
|
|
if (ws.readyState === ws.OPEN) { |
|
|
ws.send(JSON.stringify({ type: 'data', data })); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const server = http.createServer(app); |
|
|
|
|
|
|
|
|
const wss = new WebSocketServer({ server }); |
|
|
|
|
|
wss.on('connection', (ws) => { |
|
|
console.log('🔗 New terminal connection'); |
|
|
terminals.add(ws); |
|
|
|
|
|
|
|
|
ws.send(JSON.stringify({ |
|
|
type: 'welcome', |
|
|
message: 'Connected to OpenCode CLI TUI' |
|
|
})); |
|
|
|
|
|
|
|
|
if (!opencodeProcess) { |
|
|
startOpenCode(); |
|
|
} |
|
|
|
|
|
|
|
|
ws.on('message', (message) => { |
|
|
try { |
|
|
const data = JSON.parse(message); |
|
|
|
|
|
if (data.type === 'input' && opencodeStdin) { |
|
|
|
|
|
opencodeStdin.write(data.input); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('WebSocket message error:', error); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
ws.on('close', () => { |
|
|
console.log('🔌 Terminal connection closed'); |
|
|
terminals.delete(ws); |
|
|
}); |
|
|
|
|
|
ws.on('error', (error) => { |
|
|
console.error('WebSocket error:', error); |
|
|
terminals.delete(ws); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
app.get('/health', (req, res) => { |
|
|
const isRunning = opencodeProcess !== null; |
|
|
|
|
|
res.status(isRunning ? 200 : 503).json({ |
|
|
status: isRunning ? 'ok' : 'error', |
|
|
timestamp: new Date().toISOString(), |
|
|
service: 'OpenCode CLI TUI Web', |
|
|
opencode_running: isRunning, |
|
|
active_terminals: terminals.size |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
app.post('/api/restart', (req, res) => { |
|
|
if (opencodeProcess) { |
|
|
opencodeProcess.kill('SIGTERM'); |
|
|
} |
|
|
|
|
|
setTimeout(() => { |
|
|
startOpenCode(); |
|
|
res.json({ success: true, message: 'OpenCode CLI restarted' }); |
|
|
}, 1000); |
|
|
}); |
|
|
|
|
|
|
|
|
app.post('/api/command', (req, res) => { |
|
|
const { command } = req.body; |
|
|
|
|
|
if (!opencodeStdin) { |
|
|
return res.status(503).json({ error: 'OpenCode CLI not running' }); |
|
|
} |
|
|
|
|
|
if (command) { |
|
|
opencodeStdin.write(command + '\n'); |
|
|
res.json({ success: true, command }); |
|
|
} else { |
|
|
res.status(400).json({ error: 'Command is required' }); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
app.get('/api', (req, res) => { |
|
|
res.json({ |
|
|
message: 'OpenCode CLI TUI Web Server', |
|
|
version: '1.0.0', |
|
|
endpoints: { |
|
|
health: '/health', |
|
|
restart: '/api/restart', |
|
|
command: '/api/command', |
|
|
websocket: 'WebSocket (ws://host:port)' |
|
|
}, |
|
|
features: { |
|
|
tui_embedding: true, |
|
|
realtime_output: true, |
|
|
multi_user: true, |
|
|
opencode_integration: true |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
app.get('/', (req, res) => { |
|
|
res.sendFile('index.html', { root: 'public' }); |
|
|
}); |
|
|
|
|
|
|
|
|
server.listen(PORT, '0.0.0.0', () => { |
|
|
console.log(`🌐 OpenCode CLI TUI Web Server running on port ${PORT}`); |
|
|
console.log(`📱 Access at: http://0.0.0.0:${PORT}`); |
|
|
console.log(`🔌 WebSocket endpoint: ws://0.0.0.0:${PORT}`); |
|
|
}); |
|
|
|
|
|
|
|
|
process.on('SIGTERM', () => { |
|
|
console.log('🛑 Received SIGTERM, shutting down gracefully...'); |
|
|
|
|
|
|
|
|
terminals.forEach((ws) => { |
|
|
ws.close(); |
|
|
}); |
|
|
|
|
|
|
|
|
if (opencodeProcess) { |
|
|
opencodeProcess.kill('SIGTERM'); |
|
|
} |
|
|
|
|
|
server.close(() => { |
|
|
console.log('🔚 Server closed'); |
|
|
process.exit(0); |
|
|
}); |
|
|
}); |
|
|
|
|
|
process.on('SIGINT', () => { |
|
|
console.log('🛑 Received SIGINT, shutting down gracefully...'); |
|
|
|
|
|
|
|
|
terminals.forEach((ws) => { |
|
|
ws.close(); |
|
|
}); |
|
|
|
|
|
|
|
|
if (opencodeProcess) { |
|
|
opencodeProcess.kill('SIGINT'); |
|
|
} |
|
|
|
|
|
server.close(() => { |
|
|
console.log('🔚 Server closed'); |
|
|
process.exit(0); |
|
|
}); |
|
|
}); |
|
|
|
|
|
export default app; |