@andrebuzeli/git-mcp 15.8.4 → 15.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -125
- package/package.json +28 -44
- package/src/index.js +146 -139
- package/src/providers/providerManager.js +203 -217
- package/src/tools/git-diff.js +137 -126
- package/src/tools/git-help.js +285 -285
- package/src/tools/git-remote.js +472 -472
- package/src/tools/git-workflow.js +403 -403
- package/src/utils/env.js +104 -104
- package/src/utils/errors.js +431 -431
- package/src/utils/gitAdapter.js +932 -951
- package/src/utils/hooks.js +255 -255
- package/src/utils/metrics.js +198 -198
- package/src/utils/providerExec.js +58 -58
- package/src/utils/repoHelpers.js +160 -160
- package/src/utils/retry.js +123 -123
- package/install.sh +0 -68
|
@@ -1,403 +1,403 @@
|
|
|
1
|
-
import Ajv from "ajv";
|
|
2
|
-
import { MCPError, asToolError, asToolResult, errorToResponse, createError } from "../utils/errors.js";
|
|
3
|
-
import { getRepoNameFromPath, detectProjectType, GITIGNORE_TEMPLATES, validateProjectPath } from "../utils/repoHelpers.js";
|
|
4
|
-
|
|
5
|
-
const ajv = new Ajv({ allErrors: true });
|
|
6
|
-
|
|
7
|
-
export function createGitWorkflowTool(pm, git) {
|
|
8
|
-
const inputSchema = {
|
|
9
|
-
type: "object",
|
|
10
|
-
properties: {
|
|
11
|
-
projectPath: {
|
|
12
|
-
type: "string",
|
|
13
|
-
description: "Caminho absoluto do diretório do projeto (ex: 'C:/Users/user/projeto' ou '/home/user/projeto')"
|
|
14
|
-
},
|
|
15
|
-
action: {
|
|
16
|
-
type: "string",
|
|
17
|
-
enum: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean", "update"],
|
|
18
|
-
description: `Ação a executar:
|
|
19
|
-
- init: Inicializa repositório git local E cria repos no GitHub/Gitea automaticamente
|
|
20
|
-
- status: Retorna arquivos modificados, staged, untracked (use ANTES de commit para ver o que mudou)
|
|
21
|
-
- add: Adiciona arquivos ao staging (use ANTES de commit)
|
|
22
|
-
- remove: Remove arquivos do staging
|
|
23
|
-
- commit: Cria commit com os arquivos staged (use DEPOIS de add)
|
|
24
|
-
- push: Envia commits para GitHub E Gitea em paralelo (use DEPOIS de commit)
|
|
25
|
-
- ensure-remotes: Configura remotes GitHub e Gitea (use se push falhar por falta de remote)
|
|
26
|
-
- clean: Remove arquivos não rastreados do working directory
|
|
27
|
-
- update: Atualiza projeto completo: init (se necessário), add, commit, ensure-remotes e push`
|
|
28
|
-
},
|
|
29
|
-
files: {
|
|
30
|
-
type: "array",
|
|
31
|
-
items: { type: "string" },
|
|
32
|
-
description: "Lista de arquivos para add/remove. Use ['.'] para todos os arquivos. Ex: ['src/index.js', 'package.json']"
|
|
33
|
-
},
|
|
34
|
-
message: {
|
|
35
|
-
type: "string",
|
|
36
|
-
description: "Mensagem do commit. Obrigatório para action='commit'. Opcional para action='update' (se não fornecido, usa mensagem padrão). Ex: 'feat: adiciona nova funcionalidade'"
|
|
37
|
-
},
|
|
38
|
-
force: {
|
|
39
|
-
type: "boolean",
|
|
40
|
-
description: "Force push (use apenas se push normal falhar com erro de histórico divergente). Default: false"
|
|
41
|
-
},
|
|
42
|
-
createGitignore: {
|
|
43
|
-
type: "boolean",
|
|
44
|
-
description: "Se true, cria .gitignore padrão baseado no tipo de projeto (action='init'). Default: true"
|
|
45
|
-
},
|
|
46
|
-
isPublic: {
|
|
47
|
-
type: "boolean",
|
|
48
|
-
description: "Se true, repositório será PÚBLICO. Default: false (privado). Aplica-se a action='init' e 'ensure-remotes'"
|
|
49
|
-
},
|
|
50
|
-
dryRun: {
|
|
51
|
-
type: "boolean",
|
|
52
|
-
description: "Se true, simula a operação sem executar (útil para testes). Default: false"
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
required: ["projectPath", "action"],
|
|
56
|
-
additionalProperties: false
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const description = `Operações Git essenciais com sincronização automática GitHub + Gitea.
|
|
60
|
-
|
|
61
|
-
FLUXO TÍPICO DE TRABALHO:
|
|
62
|
-
1. git-workflow status → ver arquivos modificados
|
|
63
|
-
2. git-workflow add → adicionar arquivos ao staging
|
|
64
|
-
3. git-workflow commit → criar commit
|
|
65
|
-
4. git-workflow push → enviar para GitHub e Gitea
|
|
66
|
-
|
|
67
|
-
QUANDO USAR CADA ACTION:
|
|
68
|
-
- status: Para verificar estado atual do repositório
|
|
69
|
-
- add: Quando há arquivos modificados para commitar
|
|
70
|
-
- commit: Após add, para salvar as mudanças
|
|
71
|
-
- push: Após commit, para sincronizar com remotes
|
|
72
|
-
- init: Apenas uma vez, para novos projetos (cria .gitignore automaticamente)
|
|
73
|
-
- ensure-remotes: Se push falhar por falta de configuração
|
|
74
|
-
- clean: Limpar arquivos não rastreados
|
|
75
|
-
- update: Para atualizar projeto completo (init + add + commit + push) - mais rápido que fazer cada ação separadamente
|
|
76
|
-
|
|
77
|
-
EXEMPLOS DE USO:
|
|
78
|
-
• Iniciar projeto: { "projectPath": "/path/to/project", "action": "init" }
|
|
79
|
-
• Ver mudanças: { "projectPath": "/path/to/project", "action": "status" }
|
|
80
|
-
• Adicionar tudo: { "projectPath": "/path/to/project", "action": "add", "files": ["."] }
|
|
81
|
-
• Commit: { "projectPath": "/path/to/project", "action": "commit", "message": "feat: add new feature" }
|
|
82
|
-
• Push: { "projectPath": "/path/to/project", "action": "push" }`;
|
|
83
|
-
|
|
84
|
-
async function handle(args) {
|
|
85
|
-
const validate = ajv.compile(inputSchema);
|
|
86
|
-
if (!validate(args || {})) {
|
|
87
|
-
return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
88
|
-
}
|
|
89
|
-
const { projectPath, action } = args;
|
|
90
|
-
try {
|
|
91
|
-
// #region agent log
|
|
92
|
-
if (process.env.DEBUG_AGENT_LOG) {
|
|
93
|
-
fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H1',location:'git-workflow.js:handle-entry',message:'handle start',data:{action,projectPath},timestamp:Date.now()})}).catch(()=>{});
|
|
94
|
-
}
|
|
95
|
-
// #endregion
|
|
96
|
-
|
|
97
|
-
validateProjectPath(projectPath);
|
|
98
|
-
// #region agent log
|
|
99
|
-
if (process.env.DEBUG_AGENT_LOG) {
|
|
100
|
-
fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H1',location:'git-workflow.js:handle-validated',message:'projectPath validated',data:{action},timestamp:Date.now()})}).catch(()=>{});
|
|
101
|
-
}
|
|
102
|
-
// #endregion
|
|
103
|
-
if (action === "init") {
|
|
104
|
-
if (args.dryRun) {
|
|
105
|
-
return asToolResult({
|
|
106
|
-
success: true,
|
|
107
|
-
dryRun: true,
|
|
108
|
-
message: "DRY RUN: Repositório seria inicializado localmente e nos providers",
|
|
109
|
-
repoName: getRepoNameFromPath(projectPath),
|
|
110
|
-
gitignoreCreated: shouldCreateGitignore
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
await git.init(projectPath);
|
|
115
|
-
|
|
116
|
-
// Criar .gitignore baseado no tipo de projeto
|
|
117
|
-
const shouldCreateGitignore = args.createGitignore !== false;
|
|
118
|
-
let gitignoreCreated = false;
|
|
119
|
-
if (shouldCreateGitignore) {
|
|
120
|
-
const projectType = detectProjectType(projectPath);
|
|
121
|
-
const patterns = GITIGNORE_TEMPLATES[projectType] || GITIGNORE_TEMPLATES.general;
|
|
122
|
-
await git.createGitignore(projectPath, patterns);
|
|
123
|
-
gitignoreCreated = true;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const repo = getRepoNameFromPath(projectPath);
|
|
127
|
-
const isPublic = args.isPublic === true; // Default: privado
|
|
128
|
-
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
|
|
129
|
-
return asToolResult({
|
|
130
|
-
success: true,
|
|
131
|
-
ensured,
|
|
132
|
-
isPrivate: !isPublic,
|
|
133
|
-
gitignoreCreated,
|
|
134
|
-
message: "Repositório inicializado localmente e nos providers" + (gitignoreCreated ? " (.gitignore criado)" : "") + (!isPublic ? " [PRIVADO]" : " [PÚBLICO]")
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
if (action === "status") {
|
|
138
|
-
const st = await git.status(projectPath);
|
|
139
|
-
// #region agent log
|
|
140
|
-
if (process.env.DEBUG_AGENT_LOG) {
|
|
141
|
-
fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H2',location:'git-workflow.js:status',message:'status result',data:{modified:st.modified.length,created:st.created.length,deleted:st.deleted.length,notAdded:st.not_added.length,isClean:st.isClean},timestamp:Date.now()})}).catch(()=>{});
|
|
142
|
-
}
|
|
143
|
-
// #endregion
|
|
144
|
-
|
|
145
|
-
if (args.dryRun) {
|
|
146
|
-
return asToolResult({
|
|
147
|
-
success: true,
|
|
148
|
-
dryRun: true,
|
|
149
|
-
message: "DRY RUN: Status seria verificado",
|
|
150
|
-
...st
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Adicionar contexto para AI Agent decidir próximo passo
|
|
155
|
-
const hasUnstaged = st.not_added?.length > 0 || st.modified?.length > 0 || st.created?.length > 0;
|
|
156
|
-
const hasStaged = st.staged?.length > 0;
|
|
157
|
-
const hasConflicts = st.conflicted?.length > 0;
|
|
158
|
-
|
|
159
|
-
let _aiContext = {
|
|
160
|
-
needsAdd: hasUnstaged && !hasStaged,
|
|
161
|
-
readyToCommit: hasStaged && !hasConflicts,
|
|
162
|
-
hasConflicts: hasConflicts,
|
|
163
|
-
isClean: st.isClean,
|
|
164
|
-
suggestedAction: null
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
if (hasConflicts) {
|
|
168
|
-
_aiContext.suggestedAction = "Resolva os conflitos manualmente antes de continuar";
|
|
169
|
-
} else if (hasStaged) {
|
|
170
|
-
_aiContext.suggestedAction = "Use action='commit' com uma mensagem descritiva";
|
|
171
|
-
} else if (hasUnstaged) {
|
|
172
|
-
_aiContext.suggestedAction = "Use action='add' com files=['.'] para adicionar todas as mudanças";
|
|
173
|
-
} else {
|
|
174
|
-
_aiContext.suggestedAction = "Working tree limpa. Modifique arquivos ou use action='push' se há commits pendentes";
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return asToolResult({ ...st, _aiContext }, { tool: 'workflow', action: 'status' });
|
|
178
|
-
}
|
|
179
|
-
if (action === "add") {
|
|
180
|
-
const files = Array.isArray(args.files) && args.files.length ? args.files : ["."];
|
|
181
|
-
|
|
182
|
-
if (args.dryRun) {
|
|
183
|
-
return asToolResult({
|
|
184
|
-
success: true,
|
|
185
|
-
dryRun: true,
|
|
186
|
-
message: `DRY RUN: Arquivos seriam adicionados: ${files.join(", ")}`,
|
|
187
|
-
files
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
await git.add(projectPath, files);
|
|
192
|
-
return asToolResult({ success: true, files }, { tool: 'workflow', action: 'add' });
|
|
193
|
-
}
|
|
194
|
-
if (action === "remove") {
|
|
195
|
-
const files = Array.isArray(args.files) ? args.files : [];
|
|
196
|
-
await git.remove(projectPath, files);
|
|
197
|
-
return asToolResult({ success: true, files });
|
|
198
|
-
}
|
|
199
|
-
if (action === "commit") {
|
|
200
|
-
if (!args.message) {
|
|
201
|
-
return asToolError("MISSING_PARAMETER", "message é obrigatório para commit", { parameter: "message" });
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (args.dryRun) {
|
|
205
|
-
return asToolResult({
|
|
206
|
-
success: true,
|
|
207
|
-
dryRun: true,
|
|
208
|
-
message: `DRY RUN: Commit seria criado com mensagem: "${args.message}"`
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const sha = await git.commit(projectPath, args.message);
|
|
213
|
-
// #region agent log
|
|
214
|
-
if (process.env.DEBUG_AGENT_LOG) {
|
|
215
|
-
fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H3',location:'git-workflow.js:commit',message:'commit created',data:{sha,message:args.message},timestamp:Date.now()})}).catch(()=>{});
|
|
216
|
-
}
|
|
217
|
-
// #endregion
|
|
218
|
-
return asToolResult({ success: true, sha, message: args.message }, { tool: 'workflow', action: 'commit' });
|
|
219
|
-
}
|
|
220
|
-
if (action === "clean") {
|
|
221
|
-
if (args.dryRun) {
|
|
222
|
-
const result = await git.cleanUntracked(projectPath);
|
|
223
|
-
return asToolResult({
|
|
224
|
-
success: true,
|
|
225
|
-
dryRun: true,
|
|
226
|
-
message: `DRY RUN: ${result.cleaned.length} arquivo(s) seriam removidos`,
|
|
227
|
-
wouldClean: result.cleaned
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const result = await git.cleanUntracked(projectPath);
|
|
232
|
-
return asToolResult({
|
|
233
|
-
success: true,
|
|
234
|
-
...result,
|
|
235
|
-
message: result.cleaned.length > 0
|
|
236
|
-
? `${result.cleaned.length} arquivo(s) não rastreados removidos`
|
|
237
|
-
: "Nenhum arquivo para limpar"
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
if (action === "ensure-remotes") {
|
|
241
|
-
const repo = getRepoNameFromPath(projectPath);
|
|
242
|
-
const isPublic = args.isPublic === true; // Default: privado
|
|
243
|
-
|
|
244
|
-
if (args.dryRun) {
|
|
245
|
-
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: false, isPublic }); // Don't create for dry run
|
|
246
|
-
const ghOwner = await pm.getGitHubOwner();
|
|
247
|
-
const geOwner = await pm.getGiteaOwner();
|
|
248
|
-
const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
|
|
249
|
-
const base = pm.giteaUrl?.replace(/\/$/, "") || "";
|
|
250
|
-
const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
|
|
251
|
-
|
|
252
|
-
return asToolResult({
|
|
253
|
-
success: true,
|
|
254
|
-
dryRun: true,
|
|
255
|
-
message: "DRY RUN: Remotes seriam configurados",
|
|
256
|
-
repo,
|
|
257
|
-
githubUrl,
|
|
258
|
-
giteaUrl,
|
|
259
|
-
ensured
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
|
|
264
|
-
const ghOwner = await pm.getGitHubOwner();
|
|
265
|
-
const geOwner = await pm.getGiteaOwner();
|
|
266
|
-
const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
|
|
267
|
-
const base = pm.giteaUrl?.replace(/\/$/, "") || "";
|
|
268
|
-
const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
|
|
269
|
-
await git.ensureRemotes(projectPath, { githubUrl, giteaUrl });
|
|
270
|
-
const remotes = await git.listRemotes(projectPath);
|
|
271
|
-
return asToolResult({ success: true, ensured, remotes, isPrivate: !isPublic }, { tool: 'workflow', action: 'ensure-remotes' });
|
|
272
|
-
}
|
|
273
|
-
if (action === "push") {
|
|
274
|
-
const branch = await git.getCurrentBranch(projectPath);
|
|
275
|
-
const force = !!args.force;
|
|
276
|
-
|
|
277
|
-
if (args.dryRun) {
|
|
278
|
-
return asToolResult({
|
|
279
|
-
success: true,
|
|
280
|
-
dryRun: true,
|
|
281
|
-
message: `DRY RUN: Push seria executado na branch '${branch}'${force ? ' (force)' : ''}`,
|
|
282
|
-
branch,
|
|
283
|
-
force
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const result = await git.pushParallel(projectPath, branch, force);
|
|
288
|
-
return asToolResult({ success: true, branch, ...result });
|
|
289
|
-
}
|
|
290
|
-
if (action === "update") {
|
|
291
|
-
if (args.dryRun) {
|
|
292
|
-
return asToolResult({
|
|
293
|
-
success: true,
|
|
294
|
-
dryRun: true,
|
|
295
|
-
message: "DRY RUN: Update completo seria executado (init se necessário, add, commit, push)"
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const results = {
|
|
300
|
-
init: null,
|
|
301
|
-
ensureRemotes: null,
|
|
302
|
-
add: null,
|
|
303
|
-
commit: null,
|
|
304
|
-
push: null
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
const repo = getRepoNameFromPath(projectPath);
|
|
308
|
-
const isPublic = args.isPublic === true;
|
|
309
|
-
|
|
310
|
-
// 1. Verificar se é repo Git, se não for, fazer init
|
|
311
|
-
const isRepo = await git.isRepo(projectPath).catch(() => false);
|
|
312
|
-
if (!isRepo) {
|
|
313
|
-
await git.init(projectPath);
|
|
314
|
-
|
|
315
|
-
// Criar .gitignore baseado no tipo de projeto
|
|
316
|
-
const shouldCreateGitignore = args.createGitignore !== false;
|
|
317
|
-
if (shouldCreateGitignore) {
|
|
318
|
-
const projectType = detectProjectType(projectPath);
|
|
319
|
-
const patterns = GITIGNORE_TEMPLATES[projectType] || GITIGNORE_TEMPLATES.general;
|
|
320
|
-
await git.createGitignore(projectPath, patterns);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
|
|
324
|
-
results.init = { success: true, ensured, isPrivate: !isPublic, gitignoreCreated: shouldCreateGitignore };
|
|
325
|
-
} else {
|
|
326
|
-
results.init = { success: true, skipped: true, message: "Repositório já existe" };
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 2. Garantir remotes configurados (sempre verifica/configura)
|
|
330
|
-
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
|
|
331
|
-
const ghOwner = await pm.getGitHubOwner();
|
|
332
|
-
const geOwner = await pm.getGiteaOwner();
|
|
333
|
-
const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
|
|
334
|
-
const base = pm.giteaUrl?.replace(/\/$/, "") || "";
|
|
335
|
-
const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
|
|
336
|
-
await git.ensureRemotes(projectPath, { githubUrl, giteaUrl });
|
|
337
|
-
const remotes = await git.listRemotes(projectPath);
|
|
338
|
-
results.ensureRemotes = { success: true, ensured, remotes };
|
|
339
|
-
|
|
340
|
-
// 3. Verificar status e fazer add se necessário
|
|
341
|
-
const status = await git.status(projectPath);
|
|
342
|
-
if (!status.isClean || status.modified?.length > 0 || status.created?.length > 0 || status.notAdded?.length > 0) {
|
|
343
|
-
await git.add(projectPath, ["."]);
|
|
344
|
-
results.add = { success: true, files: ["."] };
|
|
345
|
-
} else {
|
|
346
|
-
results.add = { success: true, skipped: true, message: "Nenhum arquivo para adicionar" };
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// 4. Verificar se há algo staged e fazer commit
|
|
350
|
-
const statusAfterAdd = await git.status(projectPath);
|
|
351
|
-
if (statusAfterAdd.staged?.length > 0) {
|
|
352
|
-
// Gerar mensagem padrão se não fornecida
|
|
353
|
-
const commitMessage = args.message || `Update: ${new Date().toISOString().split('T')[0]} - ${statusAfterAdd.staged.length} arquivo(s) modificado(s)`;
|
|
354
|
-
const sha = await git.commit(projectPath, commitMessage);
|
|
355
|
-
results.commit = { success: true, sha, message: commitMessage };
|
|
356
|
-
} else {
|
|
357
|
-
results.commit = { success: true, skipped: true, message: "Nenhum arquivo staged para commit" };
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// 5. Fazer push (só se houver commits para enviar)
|
|
361
|
-
const branch = await git.getCurrentBranch(projectPath);
|
|
362
|
-
const force = !!args.force;
|
|
363
|
-
try {
|
|
364
|
-
const pushResult = await git.pushParallel(projectPath, branch, force);
|
|
365
|
-
results.push = { success: true, branch, ...pushResult };
|
|
366
|
-
} catch (pushError) {
|
|
367
|
-
// Se push falhar mas não houver commits, não é erro crítico
|
|
368
|
-
if (results.commit?.skipped) {
|
|
369
|
-
results.push = { success: true, skipped: true, message: "Nenhum commit para enviar" };
|
|
370
|
-
} else {
|
|
371
|
-
throw pushError;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Resumo final
|
|
376
|
-
const allSuccess = Object.values(results).every(r => r?.success !== false);
|
|
377
|
-
const stepsExecuted = Object.entries(results)
|
|
378
|
-
.filter(([_, r]) => r && !r.skipped)
|
|
379
|
-
.map(([step, _]) => step);
|
|
380
|
-
|
|
381
|
-
return asToolResult({
|
|
382
|
-
success: allSuccess,
|
|
383
|
-
message: `Update completo executado: ${stepsExecuted.join(" → ")}`,
|
|
384
|
-
results,
|
|
385
|
-
stepsExecuted
|
|
386
|
-
}, { tool: 'workflow', action: 'update' });
|
|
387
|
-
}
|
|
388
|
-
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
|
|
389
|
-
availableActions: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean", "update"],
|
|
390
|
-
suggestion: "Use uma das actions disponíveis"
|
|
391
|
-
});
|
|
392
|
-
} catch (e) {
|
|
393
|
-
return errorToResponse(e);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return {
|
|
398
|
-
name: "git-workflow",
|
|
399
|
-
description,
|
|
400
|
-
inputSchema,
|
|
401
|
-
handle
|
|
402
|
-
};
|
|
403
|
-
}
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import { MCPError, asToolError, asToolResult, errorToResponse, createError } from "../utils/errors.js";
|
|
3
|
+
import { getRepoNameFromPath, detectProjectType, GITIGNORE_TEMPLATES, validateProjectPath } from "../utils/repoHelpers.js";
|
|
4
|
+
|
|
5
|
+
const ajv = new Ajv({ allErrors: true });
|
|
6
|
+
|
|
7
|
+
export function createGitWorkflowTool(pm, git) {
|
|
8
|
+
const inputSchema = {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
projectPath: {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Caminho absoluto do diretório do projeto (ex: 'C:/Users/user/projeto' ou '/home/user/projeto')"
|
|
14
|
+
},
|
|
15
|
+
action: {
|
|
16
|
+
type: "string",
|
|
17
|
+
enum: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean", "update"],
|
|
18
|
+
description: `Ação a executar:
|
|
19
|
+
- init: Inicializa repositório git local E cria repos no GitHub/Gitea automaticamente
|
|
20
|
+
- status: Retorna arquivos modificados, staged, untracked (use ANTES de commit para ver o que mudou)
|
|
21
|
+
- add: Adiciona arquivos ao staging (use ANTES de commit)
|
|
22
|
+
- remove: Remove arquivos do staging
|
|
23
|
+
- commit: Cria commit com os arquivos staged (use DEPOIS de add)
|
|
24
|
+
- push: Envia commits para GitHub E Gitea em paralelo (use DEPOIS de commit)
|
|
25
|
+
- ensure-remotes: Configura remotes GitHub e Gitea (use se push falhar por falta de remote)
|
|
26
|
+
- clean: Remove arquivos não rastreados do working directory
|
|
27
|
+
- update: Atualiza projeto completo: init (se necessário), add, commit, ensure-remotes e push`
|
|
28
|
+
},
|
|
29
|
+
files: {
|
|
30
|
+
type: "array",
|
|
31
|
+
items: { type: "string" },
|
|
32
|
+
description: "Lista de arquivos para add/remove. Use ['.'] para todos os arquivos. Ex: ['src/index.js', 'package.json']"
|
|
33
|
+
},
|
|
34
|
+
message: {
|
|
35
|
+
type: "string",
|
|
36
|
+
description: "Mensagem do commit. Obrigatório para action='commit'. Opcional para action='update' (se não fornecido, usa mensagem padrão). Ex: 'feat: adiciona nova funcionalidade'"
|
|
37
|
+
},
|
|
38
|
+
force: {
|
|
39
|
+
type: "boolean",
|
|
40
|
+
description: "Force push (use apenas se push normal falhar com erro de histórico divergente). Default: false"
|
|
41
|
+
},
|
|
42
|
+
createGitignore: {
|
|
43
|
+
type: "boolean",
|
|
44
|
+
description: "Se true, cria .gitignore padrão baseado no tipo de projeto (action='init'). Default: true"
|
|
45
|
+
},
|
|
46
|
+
isPublic: {
|
|
47
|
+
type: "boolean",
|
|
48
|
+
description: "Se true, repositório será PÚBLICO. Default: false (privado). Aplica-se a action='init' e 'ensure-remotes'"
|
|
49
|
+
},
|
|
50
|
+
dryRun: {
|
|
51
|
+
type: "boolean",
|
|
52
|
+
description: "Se true, simula a operação sem executar (útil para testes). Default: false"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
required: ["projectPath", "action"],
|
|
56
|
+
additionalProperties: false
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const description = `Operações Git essenciais com sincronização automática GitHub + Gitea.
|
|
60
|
+
|
|
61
|
+
FLUXO TÍPICO DE TRABALHO:
|
|
62
|
+
1. git-workflow status → ver arquivos modificados
|
|
63
|
+
2. git-workflow add → adicionar arquivos ao staging
|
|
64
|
+
3. git-workflow commit → criar commit
|
|
65
|
+
4. git-workflow push → enviar para GitHub e Gitea
|
|
66
|
+
|
|
67
|
+
QUANDO USAR CADA ACTION:
|
|
68
|
+
- status: Para verificar estado atual do repositório
|
|
69
|
+
- add: Quando há arquivos modificados para commitar
|
|
70
|
+
- commit: Após add, para salvar as mudanças
|
|
71
|
+
- push: Após commit, para sincronizar com remotes
|
|
72
|
+
- init: Apenas uma vez, para novos projetos (cria .gitignore automaticamente)
|
|
73
|
+
- ensure-remotes: Se push falhar por falta de configuração
|
|
74
|
+
- clean: Limpar arquivos não rastreados
|
|
75
|
+
- update: Para atualizar projeto completo (init + add + commit + push) - mais rápido que fazer cada ação separadamente
|
|
76
|
+
|
|
77
|
+
EXEMPLOS DE USO:
|
|
78
|
+
• Iniciar projeto: { "projectPath": "/path/to/project", "action": "init" }
|
|
79
|
+
• Ver mudanças: { "projectPath": "/path/to/project", "action": "status" }
|
|
80
|
+
• Adicionar tudo: { "projectPath": "/path/to/project", "action": "add", "files": ["."] }
|
|
81
|
+
• Commit: { "projectPath": "/path/to/project", "action": "commit", "message": "feat: add new feature" }
|
|
82
|
+
• Push: { "projectPath": "/path/to/project", "action": "push" }`;
|
|
83
|
+
|
|
84
|
+
async function handle(args) {
|
|
85
|
+
const validate = ajv.compile(inputSchema);
|
|
86
|
+
if (!validate(args || {})) {
|
|
87
|
+
return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
88
|
+
}
|
|
89
|
+
const { projectPath, action } = args;
|
|
90
|
+
try {
|
|
91
|
+
// #region agent log
|
|
92
|
+
if (process.env.DEBUG_AGENT_LOG) {
|
|
93
|
+
fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H1',location:'git-workflow.js:handle-entry',message:'handle start',data:{action,projectPath},timestamp:Date.now()})}).catch(()=>{});
|
|
94
|
+
}
|
|
95
|
+
// #endregion
|
|
96
|
+
|
|
97
|
+
validateProjectPath(projectPath);
|
|
98
|
+
// #region agent log
|
|
99
|
+
if (process.env.DEBUG_AGENT_LOG) {
|
|
100
|
+
fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H1',location:'git-workflow.js:handle-validated',message:'projectPath validated',data:{action},timestamp:Date.now()})}).catch(()=>{});
|
|
101
|
+
}
|
|
102
|
+
// #endregion
|
|
103
|
+
if (action === "init") {
|
|
104
|
+
if (args.dryRun) {
|
|
105
|
+
return asToolResult({
|
|
106
|
+
success: true,
|
|
107
|
+
dryRun: true,
|
|
108
|
+
message: "DRY RUN: Repositório seria inicializado localmente e nos providers",
|
|
109
|
+
repoName: getRepoNameFromPath(projectPath),
|
|
110
|
+
gitignoreCreated: shouldCreateGitignore
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await git.init(projectPath);
|
|
115
|
+
|
|
116
|
+
// Criar .gitignore baseado no tipo de projeto
|
|
117
|
+
const shouldCreateGitignore = args.createGitignore !== false;
|
|
118
|
+
let gitignoreCreated = false;
|
|
119
|
+
if (shouldCreateGitignore) {
|
|
120
|
+
const projectType = detectProjectType(projectPath);
|
|
121
|
+
const patterns = GITIGNORE_TEMPLATES[projectType] || GITIGNORE_TEMPLATES.general;
|
|
122
|
+
await git.createGitignore(projectPath, patterns);
|
|
123
|
+
gitignoreCreated = true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
127
|
+
const isPublic = args.isPublic === true; // Default: privado
|
|
128
|
+
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
|
|
129
|
+
return asToolResult({
|
|
130
|
+
success: true,
|
|
131
|
+
ensured,
|
|
132
|
+
isPrivate: !isPublic,
|
|
133
|
+
gitignoreCreated,
|
|
134
|
+
message: "Repositório inicializado localmente e nos providers" + (gitignoreCreated ? " (.gitignore criado)" : "") + (!isPublic ? " [PRIVADO]" : " [PÚBLICO]")
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (action === "status") {
|
|
138
|
+
const st = await git.status(projectPath);
|
|
139
|
+
// #region agent log
|
|
140
|
+
if (process.env.DEBUG_AGENT_LOG) {
|
|
141
|
+
fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H2',location:'git-workflow.js:status',message:'status result',data:{modified:st.modified.length,created:st.created.length,deleted:st.deleted.length,notAdded:st.not_added.length,isClean:st.isClean},timestamp:Date.now()})}).catch(()=>{});
|
|
142
|
+
}
|
|
143
|
+
// #endregion
|
|
144
|
+
|
|
145
|
+
if (args.dryRun) {
|
|
146
|
+
return asToolResult({
|
|
147
|
+
success: true,
|
|
148
|
+
dryRun: true,
|
|
149
|
+
message: "DRY RUN: Status seria verificado",
|
|
150
|
+
...st
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Adicionar contexto para AI Agent decidir próximo passo
|
|
155
|
+
const hasUnstaged = st.not_added?.length > 0 || st.modified?.length > 0 || st.created?.length > 0;
|
|
156
|
+
const hasStaged = st.staged?.length > 0;
|
|
157
|
+
const hasConflicts = st.conflicted?.length > 0;
|
|
158
|
+
|
|
159
|
+
let _aiContext = {
|
|
160
|
+
needsAdd: hasUnstaged && !hasStaged,
|
|
161
|
+
readyToCommit: hasStaged && !hasConflicts,
|
|
162
|
+
hasConflicts: hasConflicts,
|
|
163
|
+
isClean: st.isClean,
|
|
164
|
+
suggestedAction: null
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (hasConflicts) {
|
|
168
|
+
_aiContext.suggestedAction = "Resolva os conflitos manualmente antes de continuar";
|
|
169
|
+
} else if (hasStaged) {
|
|
170
|
+
_aiContext.suggestedAction = "Use action='commit' com uma mensagem descritiva";
|
|
171
|
+
} else if (hasUnstaged) {
|
|
172
|
+
_aiContext.suggestedAction = "Use action='add' com files=['.'] para adicionar todas as mudanças";
|
|
173
|
+
} else {
|
|
174
|
+
_aiContext.suggestedAction = "Working tree limpa. Modifique arquivos ou use action='push' se há commits pendentes";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return asToolResult({ ...st, _aiContext }, { tool: 'workflow', action: 'status' });
|
|
178
|
+
}
|
|
179
|
+
if (action === "add") {
|
|
180
|
+
const files = Array.isArray(args.files) && args.files.length ? args.files : ["."];
|
|
181
|
+
|
|
182
|
+
if (args.dryRun) {
|
|
183
|
+
return asToolResult({
|
|
184
|
+
success: true,
|
|
185
|
+
dryRun: true,
|
|
186
|
+
message: `DRY RUN: Arquivos seriam adicionados: ${files.join(", ")}`,
|
|
187
|
+
files
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await git.add(projectPath, files);
|
|
192
|
+
return asToolResult({ success: true, files }, { tool: 'workflow', action: 'add' });
|
|
193
|
+
}
|
|
194
|
+
if (action === "remove") {
|
|
195
|
+
const files = Array.isArray(args.files) ? args.files : [];
|
|
196
|
+
await git.remove(projectPath, files);
|
|
197
|
+
return asToolResult({ success: true, files });
|
|
198
|
+
}
|
|
199
|
+
if (action === "commit") {
|
|
200
|
+
if (!args.message) {
|
|
201
|
+
return asToolError("MISSING_PARAMETER", "message é obrigatório para commit", { parameter: "message" });
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (args.dryRun) {
|
|
205
|
+
return asToolResult({
|
|
206
|
+
success: true,
|
|
207
|
+
dryRun: true,
|
|
208
|
+
message: `DRY RUN: Commit seria criado com mensagem: "${args.message}"`
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const sha = await git.commit(projectPath, args.message);
|
|
213
|
+
// #region agent log
|
|
214
|
+
if (process.env.DEBUG_AGENT_LOG) {
|
|
215
|
+
fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H3',location:'git-workflow.js:commit',message:'commit created',data:{sha,message:args.message},timestamp:Date.now()})}).catch(()=>{});
|
|
216
|
+
}
|
|
217
|
+
// #endregion
|
|
218
|
+
return asToolResult({ success: true, sha, message: args.message }, { tool: 'workflow', action: 'commit' });
|
|
219
|
+
}
|
|
220
|
+
if (action === "clean") {
|
|
221
|
+
if (args.dryRun) {
|
|
222
|
+
const result = await git.cleanUntracked(projectPath);
|
|
223
|
+
return asToolResult({
|
|
224
|
+
success: true,
|
|
225
|
+
dryRun: true,
|
|
226
|
+
message: `DRY RUN: ${result.cleaned.length} arquivo(s) seriam removidos`,
|
|
227
|
+
wouldClean: result.cleaned
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const result = await git.cleanUntracked(projectPath);
|
|
232
|
+
return asToolResult({
|
|
233
|
+
success: true,
|
|
234
|
+
...result,
|
|
235
|
+
message: result.cleaned.length > 0
|
|
236
|
+
? `${result.cleaned.length} arquivo(s) não rastreados removidos`
|
|
237
|
+
: "Nenhum arquivo para limpar"
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (action === "ensure-remotes") {
|
|
241
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
242
|
+
const isPublic = args.isPublic === true; // Default: privado
|
|
243
|
+
|
|
244
|
+
if (args.dryRun) {
|
|
245
|
+
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: false, isPublic }); // Don't create for dry run
|
|
246
|
+
const ghOwner = await pm.getGitHubOwner();
|
|
247
|
+
const geOwner = await pm.getGiteaOwner();
|
|
248
|
+
const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
|
|
249
|
+
const base = pm.giteaUrl?.replace(/\/$/, "") || "";
|
|
250
|
+
const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
|
|
251
|
+
|
|
252
|
+
return asToolResult({
|
|
253
|
+
success: true,
|
|
254
|
+
dryRun: true,
|
|
255
|
+
message: "DRY RUN: Remotes seriam configurados",
|
|
256
|
+
repo,
|
|
257
|
+
githubUrl,
|
|
258
|
+
giteaUrl,
|
|
259
|
+
ensured
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
|
|
264
|
+
const ghOwner = await pm.getGitHubOwner();
|
|
265
|
+
const geOwner = await pm.getGiteaOwner();
|
|
266
|
+
const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
|
|
267
|
+
const base = pm.giteaUrl?.replace(/\/$/, "") || "";
|
|
268
|
+
const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
|
|
269
|
+
await git.ensureRemotes(projectPath, { githubUrl, giteaUrl });
|
|
270
|
+
const remotes = await git.listRemotes(projectPath);
|
|
271
|
+
return asToolResult({ success: true, ensured, remotes, isPrivate: !isPublic }, { tool: 'workflow', action: 'ensure-remotes' });
|
|
272
|
+
}
|
|
273
|
+
if (action === "push") {
|
|
274
|
+
const branch = await git.getCurrentBranch(projectPath);
|
|
275
|
+
const force = !!args.force;
|
|
276
|
+
|
|
277
|
+
if (args.dryRun) {
|
|
278
|
+
return asToolResult({
|
|
279
|
+
success: true,
|
|
280
|
+
dryRun: true,
|
|
281
|
+
message: `DRY RUN: Push seria executado na branch '${branch}'${force ? ' (force)' : ''}`,
|
|
282
|
+
branch,
|
|
283
|
+
force
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const result = await git.pushParallel(projectPath, branch, force);
|
|
288
|
+
return asToolResult({ success: true, branch, ...result });
|
|
289
|
+
}
|
|
290
|
+
if (action === "update") {
|
|
291
|
+
if (args.dryRun) {
|
|
292
|
+
return asToolResult({
|
|
293
|
+
success: true,
|
|
294
|
+
dryRun: true,
|
|
295
|
+
message: "DRY RUN: Update completo seria executado (init se necessário, add, commit, push)"
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const results = {
|
|
300
|
+
init: null,
|
|
301
|
+
ensureRemotes: null,
|
|
302
|
+
add: null,
|
|
303
|
+
commit: null,
|
|
304
|
+
push: null
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
308
|
+
const isPublic = args.isPublic === true;
|
|
309
|
+
|
|
310
|
+
// 1. Verificar se é repo Git, se não for, fazer init
|
|
311
|
+
const isRepo = await git.isRepo(projectPath).catch(() => false);
|
|
312
|
+
if (!isRepo) {
|
|
313
|
+
await git.init(projectPath);
|
|
314
|
+
|
|
315
|
+
// Criar .gitignore baseado no tipo de projeto
|
|
316
|
+
const shouldCreateGitignore = args.createGitignore !== false;
|
|
317
|
+
if (shouldCreateGitignore) {
|
|
318
|
+
const projectType = detectProjectType(projectPath);
|
|
319
|
+
const patterns = GITIGNORE_TEMPLATES[projectType] || GITIGNORE_TEMPLATES.general;
|
|
320
|
+
await git.createGitignore(projectPath, patterns);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
|
|
324
|
+
results.init = { success: true, ensured, isPrivate: !isPublic, gitignoreCreated: shouldCreateGitignore };
|
|
325
|
+
} else {
|
|
326
|
+
results.init = { success: true, skipped: true, message: "Repositório já existe" };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 2. Garantir remotes configurados (sempre verifica/configura)
|
|
330
|
+
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
|
|
331
|
+
const ghOwner = await pm.getGitHubOwner();
|
|
332
|
+
const geOwner = await pm.getGiteaOwner();
|
|
333
|
+
const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
|
|
334
|
+
const base = pm.giteaUrl?.replace(/\/$/, "") || "";
|
|
335
|
+
const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
|
|
336
|
+
await git.ensureRemotes(projectPath, { githubUrl, giteaUrl });
|
|
337
|
+
const remotes = await git.listRemotes(projectPath);
|
|
338
|
+
results.ensureRemotes = { success: true, ensured, remotes };
|
|
339
|
+
|
|
340
|
+
// 3. Verificar status e fazer add se necessário
|
|
341
|
+
const status = await git.status(projectPath);
|
|
342
|
+
if (!status.isClean || status.modified?.length > 0 || status.created?.length > 0 || status.notAdded?.length > 0) {
|
|
343
|
+
await git.add(projectPath, ["."]);
|
|
344
|
+
results.add = { success: true, files: ["."] };
|
|
345
|
+
} else {
|
|
346
|
+
results.add = { success: true, skipped: true, message: "Nenhum arquivo para adicionar" };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 4. Verificar se há algo staged e fazer commit
|
|
350
|
+
const statusAfterAdd = await git.status(projectPath);
|
|
351
|
+
if (statusAfterAdd.staged?.length > 0) {
|
|
352
|
+
// Gerar mensagem padrão se não fornecida
|
|
353
|
+
const commitMessage = args.message || `Update: ${new Date().toISOString().split('T')[0]} - ${statusAfterAdd.staged.length} arquivo(s) modificado(s)`;
|
|
354
|
+
const sha = await git.commit(projectPath, commitMessage);
|
|
355
|
+
results.commit = { success: true, sha, message: commitMessage };
|
|
356
|
+
} else {
|
|
357
|
+
results.commit = { success: true, skipped: true, message: "Nenhum arquivo staged para commit" };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 5. Fazer push (só se houver commits para enviar)
|
|
361
|
+
const branch = await git.getCurrentBranch(projectPath);
|
|
362
|
+
const force = !!args.force;
|
|
363
|
+
try {
|
|
364
|
+
const pushResult = await git.pushParallel(projectPath, branch, force);
|
|
365
|
+
results.push = { success: true, branch, ...pushResult };
|
|
366
|
+
} catch (pushError) {
|
|
367
|
+
// Se push falhar mas não houver commits, não é erro crítico
|
|
368
|
+
if (results.commit?.skipped) {
|
|
369
|
+
results.push = { success: true, skipped: true, message: "Nenhum commit para enviar" };
|
|
370
|
+
} else {
|
|
371
|
+
throw pushError;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Resumo final
|
|
376
|
+
const allSuccess = Object.values(results).every(r => r?.success !== false);
|
|
377
|
+
const stepsExecuted = Object.entries(results)
|
|
378
|
+
.filter(([_, r]) => r && !r.skipped)
|
|
379
|
+
.map(([step, _]) => step);
|
|
380
|
+
|
|
381
|
+
return asToolResult({
|
|
382
|
+
success: allSuccess,
|
|
383
|
+
message: `Update completo executado: ${stepsExecuted.join(" → ")}`,
|
|
384
|
+
results,
|
|
385
|
+
stepsExecuted
|
|
386
|
+
}, { tool: 'workflow', action: 'update' });
|
|
387
|
+
}
|
|
388
|
+
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
|
|
389
|
+
availableActions: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean", "update"],
|
|
390
|
+
suggestion: "Use uma das actions disponíveis"
|
|
391
|
+
});
|
|
392
|
+
} catch (e) {
|
|
393
|
+
return errorToResponse(e);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
name: "git-workflow",
|
|
399
|
+
description,
|
|
400
|
+
inputSchema,
|
|
401
|
+
handle
|
|
402
|
+
};
|
|
403
|
+
}
|