From 9b49e9d327e870bec43e6355bc2b578df616d7e8 Mon Sep 17 00:00:00 2001 From: mark Date: Sat, 7 Mar 2026 21:36:24 +0000 Subject: [PATCH] feat: add sync-server for agent runner git-pull trigger --- sync-server.js | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 sync-server.js diff --git a/sync-server.js b/sync-server.js new file mode 100644 index 0000000..402d12e --- /dev/null +++ b/sync-server.js @@ -0,0 +1,72 @@ +#!/usr/bin/env node +/** + * sync-server.js — runs inside the Theia container on port 3001 + * Called by vibn-agent-runner after an agent session auto-commits to Gitea. + * Triggers git pull (or clone) so "Open in Theia" immediately shows new files. + * + * POST /sync { repo: "mark/sportsy" } + * GET /health + */ +const http = require('http'); +const { execSync } = require('child_process'); + +const WORKSPACE = '/home/project'; +const GITEA_BASE = process.env.GITEA_API_URL || 'https://git.vibnai.com'; +const GITEA_TOKEN = process.env.GITEA_TOKEN || process.env.GITEA_API_TOKEN || ''; +const PORT = 3001; + +const server = http.createServer((req, res) => { + res.setHeader('Content-Type', 'application/json'); + + if (req.url === '/health' && req.method === 'GET') { + res.writeHead(200).end(JSON.stringify({ ok: true, workspace: WORKSPACE })); + return; + } + + if (req.url === '/sync' && req.method === 'POST') { + let body = ''; + req.on('data', d => { body += d; }); + req.on('end', () => { + try { + const { repo } = JSON.parse(body || '{}'); + if (!repo || !/^[\w.-]+\/[\w.-]+$/.test(repo)) { + res.writeHead(400).end(JSON.stringify({ error: 'invalid repo' })); + return; + } + + const repoName = repo.split('/').pop(); + const cloneDir = `${WORKSPACE}/${repoName}`; + const authedUrl = GITEA_BASE.replace('https://', `https://vibn:${GITEA_TOKEN}@`) + `/${repo}.git`; + + let action; + try { + if (require('fs').existsSync(`${cloneDir}/.git`)) { + execSync(`git -C "${cloneDir}" fetch --depth=50 origin main 2>&1`, { timeout: 30000 }); + execSync(`git -C "${cloneDir}" reset --hard origin/main 2>&1`, { timeout: 10000 }); + action = 'pulled'; + } else { + execSync(`git clone --depth=50 "${authedUrl}" "${cloneDir}" 2>&1`, { timeout: 60000 }); + action = 'cloned'; + } + } catch (gitErr) { + console.error('[sync-server] git error:', gitErr.message); + res.writeHead(500).end(JSON.stringify({ ok: false, error: gitErr.message })); + return; + } + + console.log(`[sync-server] ${action} ${repo} -> ${cloneDir}`); + res.writeHead(200).end(JSON.stringify({ ok: true, action, repo, path: cloneDir })); + } catch (err) { + console.error('[sync-server] error:', err.message); + res.writeHead(500).end(JSON.stringify({ ok: false, error: err.message })); + } + }); + return; + } + + res.writeHead(404).end(JSON.stringify({ error: 'not found' })); +}); + +server.listen(PORT, '0.0.0.0', () => { + console.log(`[sync-server] Listening on :${PORT} - workspace: ${WORKSPACE}`); +});