/** * Local Zapier interface simulator + trace console. * Default http://0.0.0.0:3847 */ import http from 'node:http'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { ACTIONS, Simulator } from './pipeline.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const PUBLIC = path.join(__dirname, '..', 'public'); const PORT = Number(process.env.SIM_PORT || 3847); const BIND = process.env.SIM_BIND || '0.0.0.0'; const world = new Simulator(); const mime = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8', '.svg': 'image/svg+xml', }; function send(res, code, body, type = 'application/json; charset=utf-8') { const buf = typeof body === 'string' ? body : JSON.stringify(body, null, 2); res.writeHead(code, { 'content-type': type, 'cache-control': 'no-store' }); res.end(buf); } function readBody(req) { return new Promise((resolve, reject) => { const chunks = []; req.on('data', (c) => chunks.push(c)); req.on('end', () => { const raw = Buffer.concat(chunks).toString('utf8'); if (!raw) return resolve({}); try { resolve(JSON.parse(raw)); } catch (err) { reject(err); } }); req.on('error', reject); }); } const server = http.createServer(async (req, res) => { const url = new URL(req.url, `http://127.0.0.1:${PORT}`); try { if (req.method === 'GET' && url.pathname === '/health') { return send(res, 200, { ok: true, port: PORT }); } if (req.method === 'GET' && url.pathname === '/api/actions') { return send(res, 200, { actions: ACTIONS, state: world.snapshot() }); } if (req.method === 'GET' && url.pathname === '/api/state') { return send(res, 200, world.snapshot()); } if (req.method === 'POST' && url.pathname === '/api/reset') { world.reset(); return send(res, 200, { ok: true, state: world.snapshot() }); } if (req.method === 'POST' && url.pathname === '/api/run') { const body = await readBody(req); const result = await world.run(body); return send(res, 200, result); } let file = url.pathname === '/' ? '/index.html' : url.pathname; const dest = path.normalize(path.join(PUBLIC, file)); if (!dest.startsWith(PUBLIC)) { return send(res, 403, { error: 'forbidden' }); } if (!fs.existsSync(dest) || fs.statSync(dest).isDirectory()) { return send(res, 404, { error: 'not found' }); } const ext = path.extname(dest); res.writeHead(200, { 'content-type': mime[ext] || 'application/octet-stream' }); fs.createReadStream(dest).pipe(res); } catch (err) { send(res, 500, { error: err.message }); } }); server.listen(PORT, BIND, () => { process.stdout.write(`Zapier simulator http://${BIND}:${PORT}/\n`); });