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.' });
|
res.json({ ok: true, message: 'Stop signal sent — agent will halt after current step.' });
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Session may have already finished
|
|
||||||
res.json({ ok: true, message: 'Session not active (may have already completed).' });
|
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)
|
// Generate — thin structured-generation endpoint (no session, no system prompt)
|
||||||
// Use this for one-shot tasks like architecture recommendations.
|
// Use this for one-shot tasks like architecture recommendations.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -449,11 +449,94 @@ app.post('/agent/stop', (req: Request, res: Response) => {
|
|||||||
session.stopped = true;
|
session.stopped = true;
|
||||||
res.json({ ok: true, message: 'Stop signal sent — agent will halt after current step.' });
|
res.json({ ok: true, message: 'Stop signal sent — agent will halt after current step.' });
|
||||||
} else {
|
} else {
|
||||||
// Session may have already finished
|
|
||||||
res.json({ ok: true, message: 'Session not active (may have already completed).' });
|
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)
|
// Generate — thin structured-generation endpoint (no session, no system prompt)
|
||||||
// Use this for one-shot tasks like architecture recommendations.
|
// Use this for one-shot tasks like architecture recommendations.
|
||||||
|
|||||||
Reference in New Issue
Block a user