File size: 2,905 Bytes
010bc6c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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}`);
});