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:
71
dist/server.js
vendored
71
dist/server.js
vendored
@@ -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.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user