Integrar repositórios Git na execução de agentes e pipelines
- Módulo git-integration: clone/pull, commit/push automático, listagem de repos - Seletor de repositório nos modais de execução (agente e pipeline) - Seletor de branch carregado dinamicamente ao escolher repo - Campo de diretório escondido quando repositório selecionado - Auto-commit e push ao final da execução com mensagem descritiva - Instrução injetada para agentes não fazerem operações git - Rotas API: GET /repos, GET /repos/:name/branches - Pipeline: commit automático ao final de todos os steps
This commit is contained in:
@@ -999,6 +999,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="execute-repo">Repositório Git</label>
|
||||||
|
<div class="repo-selector">
|
||||||
|
<select class="select" id="execute-repo">
|
||||||
|
<option value="">Nenhum (usar diretório manual)</option>
|
||||||
|
</select>
|
||||||
|
<select class="select" id="execute-repo-branch" style="display:none">
|
||||||
|
<option value="">Branch padrão</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<p class="form-hint">Se selecionado, o agente trabalha no repositório e faz commit/push automático ao finalizar.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="execute-workdir-group">
|
||||||
<label class="form-label" for="execute-workdir">Diretório de Trabalho</label>
|
<label class="form-label" for="execute-workdir">Diretório de Trabalho</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -1232,6 +1245,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="pipeline-execute-repo">Repositório Git</label>
|
||||||
|
<div class="repo-selector">
|
||||||
|
<select class="select" id="pipeline-execute-repo">
|
||||||
|
<option value="">Nenhum (usar diretório manual)</option>
|
||||||
|
</select>
|
||||||
|
<select class="select" id="pipeline-execute-repo-branch" style="display:none">
|
||||||
|
<option value="">Branch padrão</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<p class="form-hint">Se selecionado, todos os agentes trabalham no repositório e o commit/push é automático ao final.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="pipeline-execute-workdir-group">
|
||||||
<label class="form-label" for="pipeline-execute-workdir">Diretório de Trabalho (opcional)</label>
|
<label class="form-label" for="pipeline-execute-workdir">Diretório de Trabalho (opcional)</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -5301,6 +5301,9 @@ body, .sidebar, .header, .card, .modal-content, .input, .select, textarea, .metr
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo-selector { display: flex; gap: 8px; }
|
||||||
|
.repo-selector .select { flex: 1; }
|
||||||
|
|
||||||
.btn-publish { color: var(--success); }
|
.btn-publish { color: var(--success); }
|
||||||
.btn-publish:hover { background: rgba(16, 185, 129, 0.1); }
|
.btn-publish:hover { background: rgba(16, 185, 129, 0.1); }
|
||||||
|
|
||||||
|
|||||||
@@ -38,9 +38,10 @@ const API = {
|
|||||||
create(data) { return API.request('POST', '/agents', data); },
|
create(data) { return API.request('POST', '/agents', data); },
|
||||||
update(id, data) { return API.request('PUT', `/agents/${id}`, data); },
|
update(id, data) { return API.request('PUT', `/agents/${id}`, data); },
|
||||||
delete(id) { return API.request('DELETE', `/agents/${id}`); },
|
delete(id) { return API.request('DELETE', `/agents/${id}`); },
|
||||||
execute(id, task, instructions, contextFiles, workingDirectory) {
|
execute(id, task, instructions, contextFiles, workingDirectory, repoName, repoBranch) {
|
||||||
const body = { task, instructions };
|
const body = { task, instructions };
|
||||||
if (workingDirectory) body.workingDirectory = workingDirectory;
|
if (repoName) { body.repoName = repoName; if (repoBranch) body.repoBranch = repoBranch; }
|
||||||
|
else if (workingDirectory) body.workingDirectory = workingDirectory;
|
||||||
if (contextFiles && contextFiles.length > 0) body.contextFiles = contextFiles;
|
if (contextFiles && contextFiles.length > 0) body.contextFiles = contextFiles;
|
||||||
return API.request('POST', `/agents/${id}/execute`, body);
|
return API.request('POST', `/agents/${id}/execute`, body);
|
||||||
},
|
},
|
||||||
@@ -83,9 +84,10 @@ const API = {
|
|||||||
create(data) { return API.request('POST', '/pipelines', data); },
|
create(data) { return API.request('POST', '/pipelines', data); },
|
||||||
update(id, data) { return API.request('PUT', `/pipelines/${id}`, data); },
|
update(id, data) { return API.request('PUT', `/pipelines/${id}`, data); },
|
||||||
delete(id) { return API.request('DELETE', `/pipelines/${id}`); },
|
delete(id) { return API.request('DELETE', `/pipelines/${id}`); },
|
||||||
execute(id, input, workingDirectory, contextFiles) {
|
execute(id, input, workingDirectory, contextFiles, repoName, repoBranch) {
|
||||||
const body = { input };
|
const body = { input };
|
||||||
if (workingDirectory) body.workingDirectory = workingDirectory;
|
if (repoName) { body.repoName = repoName; if (repoBranch) body.repoBranch = repoBranch; }
|
||||||
|
else if (workingDirectory) body.workingDirectory = workingDirectory;
|
||||||
if (contextFiles && contextFiles.length > 0) body.contextFiles = contextFiles;
|
if (contextFiles && contextFiles.length > 0) body.contextFiles = contextFiles;
|
||||||
return API.request('POST', `/pipelines/${id}/execute`, body);
|
return API.request('POST', `/pipelines/${id}/execute`, body);
|
||||||
},
|
},
|
||||||
@@ -142,6 +144,11 @@ const API = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
repos: {
|
||||||
|
list() { return API.request('GET', '/repos'); },
|
||||||
|
branches(name) { return API.request('GET', `/repos/${encodeURIComponent(name)}/branches`); },
|
||||||
|
},
|
||||||
|
|
||||||
files: {
|
files: {
|
||||||
list(path) { return API.request('GET', `/files${path ? '?path=' + encodeURIComponent(path) : ''}`); },
|
list(path) { return API.request('GET', `/files${path ? '?path=' + encodeURIComponent(path) : ''}`); },
|
||||||
delete(path) { return API.request('DELETE', `/files?path=${encodeURIComponent(path)}`); },
|
delete(path) { return API.request('DELETE', `/files?path=${encodeURIComponent(path)}`); },
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const App = {
|
|||||||
|
|
||||||
App._executeDropzone = Utils.initDropzone('execute-dropzone', 'execute-files', 'execute-file-list');
|
App._executeDropzone = Utils.initDropzone('execute-dropzone', 'execute-files', 'execute-file-list');
|
||||||
App._pipelineDropzone = Utils.initDropzone('pipeline-execute-dropzone', 'pipeline-execute-files', 'pipeline-execute-file-list');
|
App._pipelineDropzone = Utils.initDropzone('pipeline-execute-dropzone', 'pipeline-execute-files', 'pipeline-execute-file-list');
|
||||||
|
App._initRepoSelectors();
|
||||||
|
|
||||||
const initialSection = location.hash.replace('#', '') || 'dashboard';
|
const initialSection = location.hash.replace('#', '') || 'dashboard';
|
||||||
App.navigateTo(App.sections.includes(initialSection) ? initialSection : 'dashboard');
|
App.navigateTo(App.sections.includes(initialSection) ? initialSection : 'dashboard');
|
||||||
@@ -860,6 +861,61 @@ const App = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_reposCache: null,
|
||||||
|
|
||||||
|
async _loadRepos(selectId) {
|
||||||
|
const select = document.getElementById(selectId);
|
||||||
|
if (!select) return;
|
||||||
|
try {
|
||||||
|
if (!App._reposCache) App._reposCache = await API.repos.list();
|
||||||
|
const current = select.value;
|
||||||
|
select.innerHTML = '<option value="">Nenhum (usar diretório manual)</option>';
|
||||||
|
App._reposCache.forEach(r => {
|
||||||
|
select.insertAdjacentHTML('beforeend',
|
||||||
|
`<option value="${Utils.escapeHtml(r.name)}">${Utils.escapeHtml(r.name)}${r.description ? ' — ' + Utils.escapeHtml(r.description.slice(0, 40)) : ''}</option>`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
if (current) select.value = current;
|
||||||
|
} catch { }
|
||||||
|
},
|
||||||
|
|
||||||
|
_initRepoSelectors() {
|
||||||
|
const pairs = [
|
||||||
|
['execute-repo', 'execute-repo-branch', 'execute-workdir-group'],
|
||||||
|
['pipeline-execute-repo', 'pipeline-execute-repo-branch', 'pipeline-execute-workdir-group'],
|
||||||
|
];
|
||||||
|
pairs.forEach(([repoId, branchId, workdirGroupId]) => {
|
||||||
|
const repoSelect = document.getElementById(repoId);
|
||||||
|
const branchSelect = document.getElementById(branchId);
|
||||||
|
const workdirGroup = document.getElementById(workdirGroupId);
|
||||||
|
if (!repoSelect) return;
|
||||||
|
|
||||||
|
repoSelect.addEventListener('change', async () => {
|
||||||
|
const repoName = repoSelect.value;
|
||||||
|
if (repoName) {
|
||||||
|
if (workdirGroup) workdirGroup.style.display = 'none';
|
||||||
|
if (branchSelect) {
|
||||||
|
branchSelect.style.display = '';
|
||||||
|
branchSelect.innerHTML = '<option value="">Branch padrão</option>';
|
||||||
|
try {
|
||||||
|
const branches = await API.repos.branches(repoName);
|
||||||
|
branches.forEach(b => {
|
||||||
|
branchSelect.insertAdjacentHTML('beforeend', `<option value="${Utils.escapeHtml(b)}">${Utils.escapeHtml(b)}</option>`);
|
||||||
|
});
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (workdirGroup) workdirGroup.style.display = '';
|
||||||
|
if (branchSelect) branchSelect.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
repoSelect.addEventListener('focus', () => {
|
||||||
|
if (repoSelect.options.length <= 1) App._loadRepos(repoId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
async _handleExecute() {
|
async _handleExecute() {
|
||||||
const agentId = document.getElementById('execute-agent-select')?.value
|
const agentId = document.getElementById('execute-agent-select')?.value
|
||||||
|| document.getElementById('execute-agent-id')?.value;
|
|| document.getElementById('execute-agent-id')?.value;
|
||||||
@@ -877,6 +933,8 @@ const App = {
|
|||||||
|
|
||||||
const instructions = document.getElementById('execute-instructions')?.value.trim() || '';
|
const instructions = document.getElementById('execute-instructions')?.value.trim() || '';
|
||||||
const workingDirectory = document.getElementById('execute-workdir')?.value.trim() || '';
|
const workingDirectory = document.getElementById('execute-workdir')?.value.trim() || '';
|
||||||
|
const repoName = document.getElementById('execute-repo')?.value || '';
|
||||||
|
const repoBranch = document.getElementById('execute-repo-branch')?.value || '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const selectEl = document.getElementById('execute-agent-select');
|
const selectEl = document.getElementById('execute-agent-select');
|
||||||
@@ -893,7 +951,7 @@ const App = {
|
|||||||
Terminal.disableChat();
|
Terminal.disableChat();
|
||||||
App._lastAgentName = agentName;
|
App._lastAgentName = agentName;
|
||||||
|
|
||||||
await API.agents.execute(agentId, task, instructions, contextFiles, workingDirectory);
|
await API.agents.execute(agentId, task, instructions, contextFiles, workingDirectory, repoName, repoBranch);
|
||||||
|
|
||||||
if (dropzone) dropzone.reset();
|
if (dropzone) dropzone.reset();
|
||||||
Modal.close('execute-modal-overlay');
|
Modal.close('execute-modal-overlay');
|
||||||
|
|||||||
@@ -374,6 +374,11 @@ const AgentsUI = {
|
|||||||
|
|
||||||
AgentsUI._loadSavedTasks();
|
AgentsUI._loadSavedTasks();
|
||||||
|
|
||||||
|
const repoSelect = document.getElementById('execute-repo');
|
||||||
|
if (repoSelect) { repoSelect.value = ''; repoSelect.dispatchEvent(new Event('change')); }
|
||||||
|
App._reposCache = null;
|
||||||
|
App._loadRepos('execute-repo');
|
||||||
|
|
||||||
Modal.open('execute-modal-overlay');
|
Modal.open('execute-modal-overlay');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Toast.error(`Erro ao abrir modal de execução: ${err.message}`);
|
Toast.error(`Erro ao abrir modal de execução: ${err.message}`);
|
||||||
|
|||||||
@@ -476,6 +476,11 @@ const PipelinesUI = {
|
|||||||
|
|
||||||
if (App._pipelineDropzone) App._pipelineDropzone.reset();
|
if (App._pipelineDropzone) App._pipelineDropzone.reset();
|
||||||
|
|
||||||
|
const repoSelect = document.getElementById('pipeline-execute-repo');
|
||||||
|
if (repoSelect) { repoSelect.value = ''; repoSelect.dispatchEvent(new Event('change')); }
|
||||||
|
App._reposCache = null;
|
||||||
|
App._loadRepos('pipeline-execute-repo');
|
||||||
|
|
||||||
Modal.open('pipeline-execute-modal-overlay');
|
Modal.open('pipeline-execute-modal-overlay');
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -503,7 +508,9 @@ const PipelinesUI = {
|
|||||||
contextFiles = uploadResult.files;
|
contextFiles = uploadResult.files;
|
||||||
}
|
}
|
||||||
|
|
||||||
await API.pipelines.execute(pipelineId, input, workingDirectory, contextFiles);
|
const repoName = document.getElementById('pipeline-execute-repo')?.value || '';
|
||||||
|
const repoBranch = document.getElementById('pipeline-execute-repo-branch')?.value || '';
|
||||||
|
await API.pipelines.execute(pipelineId, input, workingDirectory, contextFiles, repoName, repoBranch);
|
||||||
if (dropzone) dropzone.reset();
|
if (dropzone) dropzone.reset();
|
||||||
Modal.close('pipeline-execute-modal-overlay');
|
Modal.close('pipeline-execute-modal-overlay');
|
||||||
App.navigateTo('terminal');
|
App.navigateTo('terminal');
|
||||||
|
|||||||
117
src/agents/git-integration.js
Normal file
117
src/agents/git-integration.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
import { join, basename } from 'path';
|
||||||
|
|
||||||
|
const PROJECTS_DIR = '/home/projetos';
|
||||||
|
const GITEA_URL = () => process.env.GITEA_URL || 'http://gitea:3000';
|
||||||
|
const GITEA_USER = () => process.env.GITEA_USER || 'fred';
|
||||||
|
const GITEA_PASS = () => process.env.GITEA_PASS || '';
|
||||||
|
const DOMAIN = () => process.env.DOMAIN || 'nitro-cloud.duckdns.org';
|
||||||
|
|
||||||
|
function exec(cmd, cwd) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const proc = spawn('sh', ['-c', cmd], {
|
||||||
|
cwd,
|
||||||
|
env: { ...process.env, HOME: '/tmp', GIT_TERMINAL_PROMPT: '0' },
|
||||||
|
});
|
||||||
|
let stdout = '', stderr = '';
|
||||||
|
proc.stdout.on('data', d => stdout += d);
|
||||||
|
proc.stderr.on('data', d => stderr += d);
|
||||||
|
proc.on('close', code =>
|
||||||
|
code === 0 ? resolve(stdout.trim()) : reject(new Error(stderr.trim() || `exit ${code}`))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function authHeader() {
|
||||||
|
return 'Basic ' + Buffer.from(`${GITEA_USER()}:${GITEA_PASS()}`).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
function repoCloneUrl(repoName) {
|
||||||
|
return `${GITEA_URL().replace('://', `://${GITEA_USER()}:${GITEA_PASS()}@`)}/${GITEA_USER()}/${repoName}.git`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listRepos() {
|
||||||
|
const url = `${GITEA_URL()}/api/v1/user/repos?limit=50&sort=updated`;
|
||||||
|
const res = await fetch(url, { headers: { Authorization: authHeader() } });
|
||||||
|
if (!res.ok) throw new Error('Erro ao listar repositórios');
|
||||||
|
const repos = await res.json();
|
||||||
|
return repos.map(r => ({
|
||||||
|
name: r.name,
|
||||||
|
fullName: r.full_name,
|
||||||
|
description: r.description || '',
|
||||||
|
defaultBranch: r.default_branch || 'main',
|
||||||
|
updatedAt: r.updated_at,
|
||||||
|
htmlUrl: r.html_url,
|
||||||
|
cloneUrl: r.clone_url,
|
||||||
|
empty: r.empty,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listBranches(repoName) {
|
||||||
|
const url = `${GITEA_URL()}/api/v1/repos/${GITEA_USER()}/${repoName}/branches?limit=50`;
|
||||||
|
const res = await fetch(url, { headers: { Authorization: authHeader() } });
|
||||||
|
if (!res.ok) return [];
|
||||||
|
const branches = await res.json();
|
||||||
|
return branches.map(b => b.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cloneOrPull(repoName, branch) {
|
||||||
|
const targetDir = join(PROJECTS_DIR, repoName);
|
||||||
|
const cloneUrl = repoCloneUrl(repoName);
|
||||||
|
|
||||||
|
if (existsSync(join(targetDir, '.git'))) {
|
||||||
|
await exec(`git remote set-url origin "${cloneUrl}"`, targetDir);
|
||||||
|
await exec('git fetch origin', targetDir);
|
||||||
|
if (branch) {
|
||||||
|
try {
|
||||||
|
await exec(`git checkout ${branch}`, targetDir);
|
||||||
|
} catch {
|
||||||
|
await exec(`git checkout -b ${branch} origin/${branch}`, targetDir);
|
||||||
|
}
|
||||||
|
await exec(`git reset --hard origin/${branch}`, targetDir);
|
||||||
|
} else {
|
||||||
|
const currentBranch = await exec('git rev-parse --abbrev-ref HEAD', targetDir);
|
||||||
|
await exec(`git reset --hard origin/${currentBranch}`, targetDir);
|
||||||
|
}
|
||||||
|
return { dir: targetDir, action: 'pull' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const branchArg = branch ? `-b ${branch}` : '';
|
||||||
|
await exec(`git clone ${branchArg} "${cloneUrl}" "${targetDir}"`);
|
||||||
|
return { dir: targetDir, action: 'clone' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function commitAndPush(repoDir, agentName, taskSummary) {
|
||||||
|
try {
|
||||||
|
const status = await exec('git status --porcelain', repoDir);
|
||||||
|
if (!status) return { changed: false };
|
||||||
|
|
||||||
|
await exec('git add -A', repoDir);
|
||||||
|
|
||||||
|
const summary = taskSummary
|
||||||
|
? taskSummary.slice(0, 100).replace(/"/g, '\\"')
|
||||||
|
: 'Alterações automáticas';
|
||||||
|
|
||||||
|
const message = `${summary}\n\nExecutado por: ${agentName}`;
|
||||||
|
await exec(
|
||||||
|
`git -c user.name="Agents Orchestrator" -c user.email="agents@${DOMAIN()}" commit -m "${message}"`,
|
||||||
|
repoDir
|
||||||
|
);
|
||||||
|
|
||||||
|
await exec('git push origin HEAD', repoDir);
|
||||||
|
|
||||||
|
const commitHash = await exec('git rev-parse --short HEAD', repoDir);
|
||||||
|
const branch = await exec('git rev-parse --abbrev-ref HEAD', repoDir);
|
||||||
|
const repoName = basename(repoDir);
|
||||||
|
const commitUrl = `https://git.${DOMAIN()}/${GITEA_USER()}/${repoName}/commit/${commitHash}`;
|
||||||
|
|
||||||
|
return { changed: true, commitHash, branch, commitUrl, filesChanged: status.split('\n').length };
|
||||||
|
} catch (err) {
|
||||||
|
return { changed: false, error: err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjectDir(repoName) {
|
||||||
|
return join(PROJECTS_DIR, repoName);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { agentsStore, schedulesStore, executionsStore, notificationsStore, secre
|
|||||||
import * as executor from './executor.js';
|
import * as executor from './executor.js';
|
||||||
import * as scheduler from './scheduler.js';
|
import * as scheduler from './scheduler.js';
|
||||||
import { generateAgentReport } from '../reports/generator.js';
|
import { generateAgentReport } from '../reports/generator.js';
|
||||||
|
import * as gitIntegration from './git-integration.js';
|
||||||
|
|
||||||
const DEFAULT_CONFIG = {
|
const DEFAULT_CONFIG = {
|
||||||
model: 'claude-sonnet-4-6',
|
model: 'claude-sonnet-4-6',
|
||||||
@@ -180,6 +181,10 @@ export function executeTask(agentId, task, instructions, wsCallback, metadata =
|
|||||||
effectiveInstructions += `\n\n<agentes_disponiveis>\n${agentList}\n</agentes_disponiveis>`;
|
effectiveInstructions += `\n\n<agentes_disponiveis>\n${agentList}\n</agentes_disponiveis>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (metadata.repoName) {
|
||||||
|
effectiveInstructions += `\n\n<git_repository>\nVocê está trabalhando no repositório "${metadata.repoName}". NÃO faça git init, git commit, git push ou qualquer operação git. O sistema fará commit e push automaticamente ao final da execução. Foque apenas no código.\n</git_repository>`;
|
||||||
|
}
|
||||||
|
|
||||||
const effectiveConfig = { ...agent.config };
|
const effectiveConfig = { ...agent.config };
|
||||||
if (metadata.workingDirectoryOverride) {
|
if (metadata.workingDirectoryOverride) {
|
||||||
effectiveConfig.workingDirectory = metadata.workingDirectoryOverride;
|
effectiveConfig.workingDirectory = metadata.workingDirectoryOverride;
|
||||||
@@ -246,6 +251,23 @@ export function executeTask(agentId, task, instructions, wsCallback, metadata =
|
|||||||
if (cb) cb({ type: 'report_generated', executionId: execId, agentId, reportFile: report.filename });
|
if (cb) cb({ type: 'report_generated', executionId: execId, agentId, reportFile: report.filename });
|
||||||
}
|
}
|
||||||
} catch (e) { console.error('[manager] Erro ao gerar relatório:', e.message); }
|
} catch (e) { console.error('[manager] Erro ao gerar relatório:', e.message); }
|
||||||
|
if (metadata.repoName && result.result) {
|
||||||
|
const repoDir = gitIntegration.getProjectDir(metadata.repoName);
|
||||||
|
gitIntegration.commitAndPush(repoDir, agent.agent_name, taskText.slice(0, 100))
|
||||||
|
.then(gitResult => {
|
||||||
|
if (gitResult.changed) {
|
||||||
|
console.log(`[manager] Auto-commit: ${gitResult.commitHash} em ${metadata.repoName}`);
|
||||||
|
if (cb) cb({
|
||||||
|
type: 'execution_output', executionId: execId, agentId,
|
||||||
|
data: { type: 'success', content: `Git: commit ${gitResult.commitHash} pushed para ${metadata.repoName} (${gitResult.filesChanged} arquivos) → ${gitResult.commitUrl}` },
|
||||||
|
});
|
||||||
|
} else if (gitResult.error) {
|
||||||
|
console.error(`[manager] Erro no auto-commit:`, gitResult.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error(`[manager] Erro no auto-commit:`, err.message));
|
||||||
|
}
|
||||||
|
|
||||||
if (cb) cb({ type: 'execution_complete', executionId: execId, agentId, data: result });
|
if (cb) cb({ type: 'execution_complete', executionId: execId, agentId, data: result });
|
||||||
|
|
||||||
const isPipelineStep = !!metadata.pipelineExecutionId;
|
const isPipelineStep = !!metadata.pipelineExecutionId;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { pipelinesStore, agentsStore, executionsStore } from '../store/db.js';
|
import { pipelinesStore, agentsStore, executionsStore } from '../store/db.js';
|
||||||
import * as executor from './executor.js';
|
import * as executor from './executor.js';
|
||||||
|
import * as gitIntegration from './git-integration.js';
|
||||||
import { mem } from '../cache/index.js';
|
import { mem } from '../cache/index.js';
|
||||||
import { generatePipelineReport } from '../reports/generator.js';
|
import { generatePipelineReport } from '../reports/generator.js';
|
||||||
|
|
||||||
@@ -293,6 +294,19 @@ export async function executePipeline(pipelineId, initialInput, wsCallback, opti
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!pipelineState.canceled) {
|
if (!pipelineState.canceled) {
|
||||||
|
if (options.repoName) {
|
||||||
|
try {
|
||||||
|
const repoDir = gitIntegration.getProjectDir(options.repoName);
|
||||||
|
const gitResult = await gitIntegration.commitAndPush(repoDir, pl.name, `Pipeline: ${pl.name}`);
|
||||||
|
if (gitResult.changed && wsCallback) {
|
||||||
|
wsCallback({
|
||||||
|
type: 'pipeline_step_output', pipelineId, stepIndex: steps.length - 1,
|
||||||
|
data: { type: 'success', content: `Git: commit ${gitResult.commitHash} pushed para ${options.repoName} (${gitResult.filesChanged} arquivos) → ${gitResult.commitUrl}` },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) { console.error('[pipeline] Erro no auto-commit:', e.message); }
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updated = executionsStore.getById(historyRecord.id);
|
const updated = executionsStore.getById(historyRecord.id);
|
||||||
if (updated) {
|
if (updated) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import * as manager from '../agents/manager.js';
|
|||||||
import { tasksStore, settingsStore, executionsStore, webhooksStore, notificationsStore, secretsStore, agentVersionsStore } from '../store/db.js';
|
import { tasksStore, settingsStore, executionsStore, webhooksStore, notificationsStore, secretsStore, agentVersionsStore } from '../store/db.js';
|
||||||
import * as scheduler from '../agents/scheduler.js';
|
import * as scheduler from '../agents/scheduler.js';
|
||||||
import * as pipeline from '../agents/pipeline.js';
|
import * as pipeline from '../agents/pipeline.js';
|
||||||
|
import * as gitIntegration from '../agents/git-integration.js';
|
||||||
import { getBinPath, updateMaxConcurrent, cancelAllExecutions, getActiveExecutions } from '../agents/executor.js';
|
import { getBinPath, updateMaxConcurrent, cancelAllExecutions, getActiveExecutions } from '../agents/executor.js';
|
||||||
import { invalidateAgentMapCache } from '../agents/pipeline.js';
|
import { invalidateAgentMapCache } from '../agents/pipeline.js';
|
||||||
import { cached } from '../cache/index.js';
|
import { cached } from '../cache/index.js';
|
||||||
@@ -165,15 +166,24 @@ function buildContextFilesPrompt(contextFiles) {
|
|||||||
return `\n\nArquivos de contexto anexados (leia cada um deles antes de iniciar):\n${lines.join('\n')}`;
|
return `\n\nArquivos de contexto anexados (leia cada um deles antes de iniciar):\n${lines.join('\n')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
router.post('/agents/:id/execute', (req, res) => {
|
router.post('/agents/:id/execute', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { task, instructions, contextFiles, workingDirectory } = req.body;
|
const { task, instructions, contextFiles, workingDirectory, repoName, repoBranch } = req.body;
|
||||||
if (!task) return res.status(400).json({ error: 'task é obrigatório' });
|
if (!task) return res.status(400).json({ error: 'task é obrigatório' });
|
||||||
const clientId = req.headers['x-client-id'] || null;
|
const clientId = req.headers['x-client-id'] || null;
|
||||||
const filesPrompt = buildContextFilesPrompt(contextFiles);
|
const filesPrompt = buildContextFilesPrompt(contextFiles);
|
||||||
const fullTask = task + filesPrompt;
|
const fullTask = task + filesPrompt;
|
||||||
const metadata = {};
|
const metadata = {};
|
||||||
if (workingDirectory) metadata.workingDirectoryOverride = workingDirectory;
|
|
||||||
|
if (repoName) {
|
||||||
|
const syncResult = await gitIntegration.cloneOrPull(repoName, repoBranch || null);
|
||||||
|
metadata.workingDirectoryOverride = syncResult.dir;
|
||||||
|
metadata.repoName = repoName;
|
||||||
|
metadata.repoBranch = repoBranch || null;
|
||||||
|
} else if (workingDirectory) {
|
||||||
|
metadata.workingDirectoryOverride = workingDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
const executionId = manager.executeTask(req.params.id, fullTask, instructions, (msg) => wsCallback(msg, clientId), metadata);
|
const executionId = manager.executeTask(req.params.id, fullTask, instructions, (msg) => wsCallback(msg, clientId), metadata);
|
||||||
res.status(202).json({ executionId, status: 'started' });
|
res.status(202).json({ executionId, status: 'started' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -464,11 +474,20 @@ router.delete('/pipelines/:id', (req, res) => {
|
|||||||
|
|
||||||
router.post('/pipelines/:id/execute', async (req, res) => {
|
router.post('/pipelines/:id/execute', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { input, workingDirectory, contextFiles } = req.body;
|
const { input, workingDirectory, contextFiles, repoName, repoBranch } = req.body;
|
||||||
if (!input) return res.status(400).json({ error: 'input é obrigatório' });
|
if (!input) return res.status(400).json({ error: 'input é obrigatório' });
|
||||||
const clientId = req.headers['x-client-id'] || null;
|
const clientId = req.headers['x-client-id'] || null;
|
||||||
const options = {};
|
const options = {};
|
||||||
if (workingDirectory) options.workingDirectory = workingDirectory;
|
|
||||||
|
if (repoName) {
|
||||||
|
const syncResult = await gitIntegration.cloneOrPull(repoName, repoBranch || null);
|
||||||
|
options.workingDirectory = syncResult.dir;
|
||||||
|
options.repoName = repoName;
|
||||||
|
options.repoBranch = repoBranch || null;
|
||||||
|
} else if (workingDirectory) {
|
||||||
|
options.workingDirectory = workingDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
const filesPrompt = buildContextFilesPrompt(contextFiles);
|
const filesPrompt = buildContextFilesPrompt(contextFiles);
|
||||||
const fullInput = input + filesPrompt;
|
const fullInput = input + filesPrompt;
|
||||||
const result = pipeline.executePipeline(req.params.id, fullInput, (msg) => wsCallback(msg, clientId), options);
|
const result = pipeline.executePipeline(req.params.id, fullInput, (msg) => wsCallback(msg, clientId), options);
|
||||||
@@ -1160,6 +1179,24 @@ router.delete('/files', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get('/repos', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const repos = await gitIntegration.listRepos();
|
||||||
|
res.json(repos);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/repos/:name/branches', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const branches = await gitIntegration.listBranches(req.params.name);
|
||||||
|
res.json(branches);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.post('/files/publish', async (req, res) => {
|
router.post('/files/publish', async (req, res) => {
|
||||||
const { path: projectPath } = req.body;
|
const { path: projectPath } = req.body;
|
||||||
if (!projectPath) return res.status(400).json({ error: 'path é obrigatório' });
|
if (!projectPath) return res.status(400).json({ error: 'path é obrigatório' });
|
||||||
|
|||||||
Reference in New Issue
Block a user