Spaces:
Running
Running
| const express = require('express'); | |
| const cors = require('cors'); | |
| const { spawn } = require('child_process'); | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const { randomUUID } = require('crypto'); | |
| const PYTHON_BIN = process.env.BUILD123D_PYTHON || '/Users/noahk/.openclaw/workspace/skills/build123d-cad/.venv/bin/python'; | |
| const PORT = process.env.PORT || 4000; | |
| const ARTIFACT_DIR = path.join(__dirname, '..', 'artifacts'); | |
| const RUNNER_PATH = path.join(__dirname, 'runner.py'); | |
| const DIST_PATH = path.join(__dirname, '..', 'dist'); | |
| fs.mkdirSync(ARTIFACT_DIR, { recursive: true }); | |
| const app = express(); | |
| app.use(cors()); | |
| app.use(express.json({ limit: '1mb' })); | |
| app.use('/artifacts', express.static(ARTIFACT_DIR)); | |
| if (fs.existsSync(DIST_PATH)) { | |
| app.use(express.static(DIST_PATH)); | |
| } | |
| const sendSse = (res, event, data) => { | |
| res.write(`event: ${event}\n`); | |
| res.write(`data: ${JSON.stringify(data)}\n\n`); | |
| }; | |
| app.get('/api/run', (req, res) => { | |
| const code = req.query.code; | |
| if (typeof code !== 'string' || !code.trim()) { | |
| res.status(400).json({ error: 'Missing code parameter.' }); | |
| return; | |
| } | |
| res.setHeader('Content-Type', 'text/event-stream'); | |
| res.setHeader('Cache-Control', 'no-cache'); | |
| res.setHeader('Connection', 'keep-alive'); | |
| const jobId = randomUUID(); | |
| const codeFile = path.join(ARTIFACT_DIR, `${jobId}.py`); | |
| const stlFile = path.join(ARTIFACT_DIR, `${jobId}.stl`); | |
| const stepFile = path.join(ARTIFACT_DIR, `${jobId}.step`); | |
| fs.writeFileSync(codeFile, code); | |
| sendSse(res, 'log', { message: `Job ${jobId} accepted.` }); | |
| const pythonArgs = [RUNNER_PATH, '--source', codeFile, '--stl-output', stlFile, '--step-output', stepFile]; | |
| const child = spawn(PYTHON_BIN, pythonArgs, { env: process.env }); | |
| req.on('close', () => { | |
| if (!child.killed) { | |
| child.kill('SIGINT'); | |
| } | |
| }); | |
| child.stdout.on('data', (chunk) => { | |
| sendSse(res, 'log', { message: chunk.toString().trim() }); | |
| }); | |
| child.stderr.on('data', (chunk) => { | |
| sendSse(res, 'log', { message: chunk.toString().trim(), level: 'error' }); | |
| }); | |
| child.on('error', (error) => { | |
| sendSse(res, 'log', { message: `Runner error: ${error.message}`, level: 'error' }); | |
| sendSse(res, 'complete', { success: false, error: error.message }); | |
| res.end(); | |
| }); | |
| child.on('close', (code) => { | |
| const success = code === 0; | |
| if (success) { | |
| sendSse(res, 'complete', { | |
| success: true, | |
| stlPath: `/artifacts/${path.basename(stlFile)}`, | |
| stepPath: `/artifacts/${path.basename(stepFile)}` | |
| }); | |
| } else { | |
| sendSse(res, 'complete', { success: false, error: `Runner exited with ${code}` }); | |
| } | |
| res.end(); | |
| }); | |
| }); | |
| app.get('/api/health', (_req, res) => { | |
| res.json({ status: 'ok', python: PYTHON_BIN }); | |
| }); | |
| app.listen(PORT, () => { | |
| console.log(`build123d live runner listening on http://localhost:${PORT}`); | |
| }); | |