From b16a216e0e9a7b7f1f879473b9e4018134ca4a0d Mon Sep 17 00:00:00 2001 From: mawkone Date: Sat, 7 Mar 2026 11:36:47 -0800 Subject: [PATCH] =?UTF-8?q?add=20/agent/approve=20endpoint=20=E2=80=94=20c?= =?UTF-8?q?ommit,=20push=20and=20trigger=20deploy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Receives giteaRepo + commitMessage, stages all workspace changes, commits with the user-supplied message, pushes to Gitea, then optionally calls Coolify /start to trigger a rolling redeploy. Returns { committed, deployed, message } to the frontend. Made-with: Cursor --- dist/server.js | 71 ++++++++++++++++++++++++++++++++++++++++- src/server.ts | 85 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 154 insertions(+), 2 deletions(-) diff --git a/dist/server.js b/dist/server.js index c927a15..8203557 100644 --- a/dist/server.js +++ b/dist/server.js @@ -415,11 +415,80 @@ app.post('/agent/stop', (req, res) => { res.json({ ok: true, message: 'Stop signal sent — agent will halt after current step.' }); } else { - // Session may have already finished res.json({ ok: true, message: 'Session not active (may have already completed).' }); } }); // --------------------------------------------------------------------------- +// Agent Approve — commit and push agent's changes to Gitea, trigger deploy +// +// Called by vibn-frontend after the user reviews changed files and clicks +// "Approve & commit". The agent runner does git add/commit/push in the +// workspace where the agent was working. +// --------------------------------------------------------------------------- +app.post('/agent/approve', async (req, res) => { + const { giteaRepo, commitMessage, coolifyApiUrl, coolifyApiToken, coolifyAppUuid } = req.body; + if (!giteaRepo || !commitMessage) { + res.status(400).json({ error: 'giteaRepo and commitMessage are required' }); + return; + } + try { + // Resolve the workspace root for this repo (does NOT re-clone if already present) + const workspaceRoot = ensureWorkspace(giteaRepo); + // Configure git identity for this commit + const gitea = { + username: process.env.GITEA_USERNAME || 'agent', + apiToken: process.env.GITEA_API_TOKEN || '', + apiUrl: process.env.GITEA_API_URL || '', + }; + const { execSync: exec } = require('child_process'); + const gitOpts = { cwd: workspaceRoot, stdio: 'pipe' }; + // Ensure git identity + try { + exec('git config user.email "agent@vibnai.com"', gitOpts); + exec('git config user.name "VIBN Agent"', gitOpts); + } + catch { /* already set */ } + // Stage all changes + exec('git add -A', gitOpts); + // Check if there is anything to commit + let status; + try { + status = exec('git status --porcelain', gitOpts).toString().trim(); + } + catch { + status = ''; + } + if (!status) { + res.json({ ok: true, committed: false, message: 'Nothing to commit — working tree is clean.' }); + return; + } + // Commit + exec(`git commit -m ${JSON.stringify(commitMessage)}`, gitOpts); + // Push — use token auth embedded in remote URL + const authedUrl = `${gitea.apiUrl}/${giteaRepo}.git` + .replace('https://', `https://${gitea.username}:${gitea.apiToken}@`); + exec(`git push "${authedUrl}" HEAD:main`, gitOpts); + // Optionally trigger a Coolify redeploy + let deployed = false; + if (coolifyApiUrl && coolifyApiToken && coolifyAppUuid) { + try { + const deployRes = await fetch(`${coolifyApiUrl}/api/v1/applications/${coolifyAppUuid}/start`, { + method: 'POST', + headers: { Authorization: `Bearer ${coolifyApiToken}` }, + }); + deployed = deployRes.ok; + } + catch { /* deploy trigger is best-effort */ } + } + res.json({ ok: true, committed: true, deployed, message: `Committed and pushed: "${commitMessage}"` }); + } + catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error('[agent/approve]', msg); + res.status(500).json({ error: msg }); + } +}); +// --------------------------------------------------------------------------- // Generate — thin structured-generation endpoint (no session, no system prompt) // Use this for one-shot tasks like architecture recommendations. // --------------------------------------------------------------------------- diff --git a/src/server.ts b/src/server.ts index 7b6eee4..ffb1f41 100644 --- a/src/server.ts +++ b/src/server.ts @@ -449,11 +449,94 @@ app.post('/agent/stop', (req: Request, res: Response) => { session.stopped = true; res.json({ ok: true, message: 'Stop signal sent — agent will halt after current step.' }); } else { - // Session may have already finished res.json({ ok: true, message: 'Session not active (may have already completed).' }); } }); +// --------------------------------------------------------------------------- +// Agent Approve — commit and push agent's changes to Gitea, trigger deploy +// +// Called by vibn-frontend after the user reviews changed files and clicks +// "Approve & commit". The agent runner does git add/commit/push in the +// workspace where the agent was working. +// --------------------------------------------------------------------------- + +app.post('/agent/approve', async (req: Request, res: Response) => { + const { giteaRepo, commitMessage, coolifyApiUrl, coolifyApiToken, coolifyAppUuid } = req.body as { + giteaRepo?: string; + commitMessage?: string; + coolifyApiUrl?: string; + coolifyApiToken?: string; + coolifyAppUuid?: string; + }; + + if (!giteaRepo || !commitMessage) { + res.status(400).json({ error: 'giteaRepo and commitMessage are required' }); + return; + } + + try { + // Resolve the workspace root for this repo (does NOT re-clone if already present) + const workspaceRoot = ensureWorkspace(giteaRepo); + + // Configure git identity for this commit + const gitea = { + username: process.env.GITEA_USERNAME || 'agent', + apiToken: process.env.GITEA_API_TOKEN || '', + apiUrl: process.env.GITEA_API_URL || '', + }; + + const { execSync: exec } = require('child_process') as typeof import('child_process'); + const gitOpts = { cwd: workspaceRoot, stdio: 'pipe' as const }; + + // Ensure git identity + try { + exec('git config user.email "agent@vibnai.com"', gitOpts); + exec('git config user.name "VIBN Agent"', gitOpts); + } catch { /* already set */ } + + // Stage all changes + exec('git add -A', gitOpts); + + // Check if there is anything to commit + let status: string; + try { + status = exec('git status --porcelain', gitOpts).toString().trim(); + } catch { status = ''; } + + if (!status) { + res.json({ ok: true, committed: false, message: 'Nothing to commit — working tree is clean.' }); + return; + } + + // Commit + exec(`git commit -m ${JSON.stringify(commitMessage)}`, gitOpts); + + // Push — use token auth embedded in remote URL + const authedUrl = `${gitea.apiUrl}/${giteaRepo}.git` + .replace('https://', `https://${gitea.username}:${gitea.apiToken}@`); + exec(`git push "${authedUrl}" HEAD:main`, gitOpts); + + // Optionally trigger a Coolify redeploy + let deployed = false; + if (coolifyApiUrl && coolifyApiToken && coolifyAppUuid) { + try { + const deployRes = await fetch(`${coolifyApiUrl}/api/v1/applications/${coolifyAppUuid}/start`, { + method: 'POST', + headers: { Authorization: `Bearer ${coolifyApiToken}` }, + }); + deployed = deployRes.ok; + } catch { /* deploy trigger is best-effort */ } + } + + res.json({ ok: true, committed: true, deployed, message: `Committed and pushed: "${commitMessage}"` }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error('[agent/approve]', msg); + res.status(500).json({ error: msg }); + } +}); + // --------------------------------------------------------------------------- // Generate — thin structured-generation endpoint (no session, no system prompt) // Use this for one-shot tasks like architecture recommendations.