noahlee1234
Initial NaturalCAD MVP
010bc6c
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}`);
});