add /agent/approve endpoint — commit, push and trigger deploy

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
This commit is contained in:
2026-03-07 11:36:47 -08:00
parent 5aeddace91
commit b16a216e0e
2 changed files with 154 additions and 2 deletions

View File

@@ -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.