@andre.buzeli/git-mcp 16.0.8 → 16.1.2
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/package.json +32 -29
- package/src/index.js +159 -147
- package/src/tools/git-branches.js +13 -3
- package/src/tools/git-clone.js +48 -85
- package/src/tools/git-config.js +13 -3
- package/src/tools/git-diff.js +121 -137
- package/src/tools/git-files.js +13 -3
- package/src/tools/git-help.js +322 -284
- package/src/tools/git-history.js +13 -3
- package/src/tools/git-ignore.js +13 -3
- package/src/tools/git-issues.js +13 -3
- package/src/tools/git-merge.js +13 -3
- package/src/tools/git-pulls.js +14 -4
- package/src/tools/git-remote.js +503 -492
- package/src/tools/git-reset.js +23 -4
- package/src/tools/git-stash.js +13 -3
- package/src/tools/git-sync.js +13 -3
- package/src/tools/git-tags.js +13 -3
- package/src/tools/git-workflow.js +599 -456
- package/src/tools/git-worktree.js +180 -0
- package/src/utils/errors.js +434 -433
- package/src/utils/gitAdapter.js +118 -6
- package/src/utils/mcpNotify.js +45 -0
- package/src/utils/repoHelpers.js +5 -31
- package/src/utils/hooks.js +0 -255
- package/src/utils/metrics.js +0 -198
|
@@ -1,456 +1,599 @@
|
|
|
1
|
-
import Ajv from "ajv";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import path from "path";
|
|
4
|
-
import {
|
|
5
|
-
import { getRepoNameFromPath, detectProjectType, GITIGNORE_TEMPLATES, validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
},
|
|
40
|
-
|
|
41
|
-
type: "
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if (
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { asToolError, asToolResult, errorToResponse, createError } from "../utils/errors.js";
|
|
5
|
+
import { getRepoNameFromPath, detectProjectType, GITIGNORE_TEMPLATES, validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
6
|
+
import { sendLog } from "../utils/mcpNotify.js";
|
|
7
|
+
|
|
8
|
+
const ajv = new Ajv({ allErrors: true });
|
|
9
|
+
|
|
10
|
+
function resolveRemoteBranch(branchName, channel) {
|
|
11
|
+
if (channel === "production") return branchName;
|
|
12
|
+
if (channel === "beta") return `${branchName}-beta`;
|
|
13
|
+
if (channel === "alpha") return `${branchName}-alpha`;
|
|
14
|
+
return branchName;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createGitWorkflowTool(pm, git, server) {
|
|
18
|
+
const inputSchema = {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
projectPath: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Caminho absoluto do diretório do projeto no IDE (ex: '/home/user/meu-projeto' ou 'C:/Users/user/meu-projeto'). IMPORTANTE: este valor não pode ser inferido automaticamente pelo servidor — o agente deve fornecer o path real do projeto sendo trabalhado, não o diretório home do usuário."
|
|
24
|
+
},
|
|
25
|
+
action: {
|
|
26
|
+
type: "string",
|
|
27
|
+
enum: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean", "update", "init-branch"],
|
|
28
|
+
description: `Ação a executar:
|
|
29
|
+
- init: Inicializa repositório git local E cria repos no GitHub/Gitea automaticamente
|
|
30
|
+
- status: Retorna arquivos modificados, staged, untracked (use ANTES de commit para ver o que mudou)
|
|
31
|
+
- add: Adiciona arquivos ao staging (use ANTES de commit)
|
|
32
|
+
- remove: Remove arquivos do staging
|
|
33
|
+
- commit: Cria commit com os arquivos staged (use DEPOIS de add)
|
|
34
|
+
- push: Envia commits para GitHub E Gitea em paralelo (use DEPOIS de commit)
|
|
35
|
+
- ensure-remotes: Configura remotes GitHub e Gitea (use se push falhar por falta de remote)
|
|
36
|
+
- clean: Remove arquivos não rastreados do working directory
|
|
37
|
+
- update: RECOMENDADO - Executa fluxo completo automatizado (status → add → commit → push) em uma única chamada
|
|
38
|
+
- init-branch: Atalho para criar um novo worktree/branch (use git-worktree add preferencialmente)`
|
|
39
|
+
},
|
|
40
|
+
files: {
|
|
41
|
+
type: "array",
|
|
42
|
+
items: { type: "string" },
|
|
43
|
+
description: "Lista de arquivos para add/remove. Use ['.'] para todos os arquivos. Ex: ['src/index.js', 'package.json']"
|
|
44
|
+
},
|
|
45
|
+
message: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Mensagem do commit. Obrigatório para action='commit'. Ex: 'feat: adiciona nova funcionalidade'"
|
|
48
|
+
},
|
|
49
|
+
branch: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Nome da branch (para action='init-branch')"
|
|
52
|
+
},
|
|
53
|
+
channel: {
|
|
54
|
+
type: "string",
|
|
55
|
+
enum: ["production", "beta", "alpha"],
|
|
56
|
+
description: "Canal de deploy para push (action='update'). Sobrescreve o channel salvo para este push."
|
|
57
|
+
},
|
|
58
|
+
syncBranches: {
|
|
59
|
+
type: "boolean",
|
|
60
|
+
description: "Se true e executado no principal (action='update'), propaga commits para todos os worktrees registrados."
|
|
61
|
+
},
|
|
62
|
+
force: {
|
|
63
|
+
type: "boolean",
|
|
64
|
+
description: "Force push (use apenas se push normal falhar com erro de histórico divergente). Default: false"
|
|
65
|
+
},
|
|
66
|
+
createGitignore: {
|
|
67
|
+
type: "boolean",
|
|
68
|
+
description: "Se true, cria .gitignore padrão baseado no tipo de projeto (action='init'). Default: true"
|
|
69
|
+
},
|
|
70
|
+
isPublic: {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
description: "Se true, repositório será PÚBLICO. Default: false (privado). Aplica-se a action='init' e 'ensure-remotes'"
|
|
73
|
+
},
|
|
74
|
+
dryRun: {
|
|
75
|
+
type: "boolean",
|
|
76
|
+
description: "Se true, simula a operação sem executar (útil para testes). Default: false"
|
|
77
|
+
},
|
|
78
|
+
skipIfClean: {
|
|
79
|
+
type: "boolean",
|
|
80
|
+
description: "Para action='update': se true, pula silenciosamente se não houver mudanças. Default: false"
|
|
81
|
+
},
|
|
82
|
+
gitignore: {
|
|
83
|
+
type: "array",
|
|
84
|
+
items: {
|
|
85
|
+
type: "string"
|
|
86
|
+
},
|
|
87
|
+
description: "Para action='update': lista de padrões para adicionar ao .gitignore antes de atualizar"
|
|
88
|
+
},
|
|
89
|
+
organization: {
|
|
90
|
+
type: "string",
|
|
91
|
+
description: "Opcional. Nome da organização no GitHub/Gitea onde o repositório será criado/gerenciado. Se não informado, usa a conta pessoal. Ex: 'automacao-casa', 'cliente-joao'"
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
required: ["projectPath", "action"],
|
|
95
|
+
additionalProperties: false
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const description = `IMPORTANTE — projectPath:
|
|
99
|
+
Informe o caminho absoluto do projeto aberto no IDE. O servidor MCP não tem acesso ao
|
|
100
|
+
contexto do IDE e não consegue detectar automaticamente qual projeto está sendo trabalhado.
|
|
101
|
+
|
|
102
|
+
Operações Git essenciais com sincronização automática GitHub + Gitea.
|
|
103
|
+
|
|
104
|
+
⭐ RECOMENDADO - FLUXO AUTOMATIZADO:
|
|
105
|
+
• action="update" → Executa status, add, commit e push em uma única chamada
|
|
106
|
+
• Exemplo: { "projectPath": "/path", "action": "update", "message": "feat: nova feature" }
|
|
107
|
+
|
|
108
|
+
FLUXO MANUAL (se preferir controle individual):
|
|
109
|
+
1. git-workflow status → ver arquivos modificados
|
|
110
|
+
2. git-workflow add → adicionar arquivos ao staging
|
|
111
|
+
3. git-workflow commit → criar commit
|
|
112
|
+
4. git-workflow push → enviar para GitHub e Gitea
|
|
113
|
+
|
|
114
|
+
QUANDO USAR CADA ACTION:
|
|
115
|
+
- update: ⭐ RECOMENDADO - Fluxo completo automatizado (status → add → commit → push)
|
|
116
|
+
- status: Para verificar estado atual do repositório
|
|
117
|
+
- add: Quando há arquivos modificados para commitar
|
|
118
|
+
- commit: Após add, para salvar as mudanças
|
|
119
|
+
- push: Após commit, para sincronizar com remotes
|
|
120
|
+
- init: Apenas uma vez, para novos projetos (cria .gitignore automaticamente)
|
|
121
|
+
- ensure-remotes: Se push falhar por falta de configuração
|
|
122
|
+
- clean: Limpar arquivos não rastreados
|
|
123
|
+
|
|
124
|
+
EXEMPLOS DE USO:
|
|
125
|
+
• ⭐ Atualizar tudo: { "projectPath": "/path/to/project", "action": "update", "message": "feat: descrição" }
|
|
126
|
+
• Iniciar projeto: { "projectPath": "/path/to/project", "action": "init" }
|
|
127
|
+
• Ver mudanças: { "projectPath": "/path/to/project", "action": "status" }`;
|
|
128
|
+
|
|
129
|
+
async function handle(args) {
|
|
130
|
+
const validate = ajv.compile(inputSchema);
|
|
131
|
+
if (!validate(args || {})) {
|
|
132
|
+
return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
133
|
+
}
|
|
134
|
+
const { projectPath, action } = args;
|
|
135
|
+
try {
|
|
136
|
+
validateProjectPath(projectPath);
|
|
137
|
+
if (action === "init") {
|
|
138
|
+
const shouldCreateGitignore = args.createGitignore !== false;
|
|
139
|
+
|
|
140
|
+
if (args.dryRun) {
|
|
141
|
+
return asToolResult({
|
|
142
|
+
success: true,
|
|
143
|
+
dryRun: true,
|
|
144
|
+
message: "DRY RUN: Repositório seria inicializado localmente e nos providers",
|
|
145
|
+
repoName: getRepoNameFromPath(projectPath),
|
|
146
|
+
gitignoreCreated: shouldCreateGitignore
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const isRepo = await git.isRepo(projectPath);
|
|
151
|
+
if (!isRepo) {
|
|
152
|
+
await git.init(projectPath);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Criar .gitignore baseado no tipo de projeto (apenas se não existir ou se for novo repo)
|
|
156
|
+
let gitignoreCreated = false;
|
|
157
|
+
|
|
158
|
+
if (shouldCreateGitignore) {
|
|
159
|
+
const hasGitignore = fs.existsSync(path.join(projectPath, ".gitignore"));
|
|
160
|
+
if (!hasGitignore) {
|
|
161
|
+
const projectType = detectProjectType(projectPath);
|
|
162
|
+
const patterns = GITIGNORE_TEMPLATES[projectType] || GITIGNORE_TEMPLATES.general;
|
|
163
|
+
await git.createGitignore(projectPath, patterns);
|
|
164
|
+
gitignoreCreated = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
169
|
+
const isPublic = args.isPublic === true; // Default: privado
|
|
170
|
+
const organization = args.organization || undefined;
|
|
171
|
+
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic, organization });
|
|
172
|
+
|
|
173
|
+
// Configurar remotes com org se fornecida
|
|
174
|
+
const urls = await pm.getRemoteUrls(repo, organization);
|
|
175
|
+
await git.ensureRemotes(projectPath, { githubUrl: urls.github || "", giteaUrl: urls.gitea || "" });
|
|
176
|
+
|
|
177
|
+
return asToolResult({
|
|
178
|
+
success: true,
|
|
179
|
+
ensured,
|
|
180
|
+
isPrivate: !isPublic,
|
|
181
|
+
organization: organization || undefined,
|
|
182
|
+
gitignoreCreated,
|
|
183
|
+
message: "Repositório inicializado localmente e nos providers" + (organization ? ` [org: ${organization}]` : "") + (gitignoreCreated ? " (.gitignore criado)" : "") + (!isPublic ? " [PRIVADO]" : " [PÚBLICO]")
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (action === "status") {
|
|
187
|
+
const st = await git.status(projectPath);
|
|
188
|
+
|
|
189
|
+
if (args.dryRun) {
|
|
190
|
+
return asToolResult({
|
|
191
|
+
success: true,
|
|
192
|
+
dryRun: true,
|
|
193
|
+
message: "DRY RUN: Status seria verificado",
|
|
194
|
+
...st
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Adicionar contexto para AI Agent decidir próximo passo
|
|
199
|
+
const hasUnstaged = st.not_added?.length > 0 || st.modified?.length > 0 || st.created?.length > 0;
|
|
200
|
+
const hasStaged = st.staged?.length > 0;
|
|
201
|
+
const hasConflicts = st.conflicted?.length > 0;
|
|
202
|
+
|
|
203
|
+
let _aiContext = {
|
|
204
|
+
needsAdd: hasUnstaged && !hasStaged,
|
|
205
|
+
readyToCommit: hasStaged && !hasConflicts,
|
|
206
|
+
hasConflicts: hasConflicts,
|
|
207
|
+
isClean: st.isClean,
|
|
208
|
+
suggestedAction: null
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
if (hasConflicts) {
|
|
212
|
+
_aiContext.suggestedAction = "Resolva os conflitos manualmente antes de continuar";
|
|
213
|
+
} else if (hasStaged) {
|
|
214
|
+
_aiContext.suggestedAction = "Use action='commit' com uma mensagem descritiva";
|
|
215
|
+
} else if (hasUnstaged) {
|
|
216
|
+
_aiContext.suggestedAction = "Use action='add' com files=['.'] para adicionar todas as mudanças";
|
|
217
|
+
} else {
|
|
218
|
+
_aiContext.suggestedAction = "Working tree limpa. Modifique arquivos ou use action='push' se há commits pendentes";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return asToolResult({ ...st, _aiContext }, { tool: 'workflow', action: 'status' });
|
|
222
|
+
}
|
|
223
|
+
if (action === "add") {
|
|
224
|
+
const files = Array.isArray(args.files) && args.files.length ? args.files : ["."];
|
|
225
|
+
|
|
226
|
+
if (args.dryRun) {
|
|
227
|
+
return asToolResult({
|
|
228
|
+
success: true,
|
|
229
|
+
dryRun: true,
|
|
230
|
+
message: `DRY RUN: Arquivos seriam adicionados: ${files.join(", ")}`,
|
|
231
|
+
files
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
await git.add(projectPath, files);
|
|
236
|
+
return asToolResult({ success: true, files }, { tool: 'workflow', action: 'add' });
|
|
237
|
+
}
|
|
238
|
+
if (action === "remove") {
|
|
239
|
+
const files = Array.isArray(args.files) ? args.files : [];
|
|
240
|
+
await git.remove(projectPath, files);
|
|
241
|
+
return asToolResult({ success: true, files });
|
|
242
|
+
}
|
|
243
|
+
if (action === "commit") {
|
|
244
|
+
if (!args.message) {
|
|
245
|
+
return asToolError("MISSING_PARAMETER", "message é obrigatório para commit", { parameter: "message" });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (args.dryRun) {
|
|
249
|
+
return asToolResult({
|
|
250
|
+
success: true,
|
|
251
|
+
dryRun: true,
|
|
252
|
+
message: `DRY RUN: Commit seria criado com mensagem: "${args.message}"`
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const sha = await git.commit(projectPath, args.message);
|
|
257
|
+
return asToolResult({ success: true, sha, message: args.message }, { tool: 'workflow', action: 'commit' });
|
|
258
|
+
}
|
|
259
|
+
if (action === "clean") {
|
|
260
|
+
if (args.dryRun) {
|
|
261
|
+
const result = await git.cleanUntracked(projectPath);
|
|
262
|
+
return asToolResult({
|
|
263
|
+
success: true,
|
|
264
|
+
dryRun: true,
|
|
265
|
+
message: `DRY RUN: ${result.cleaned.length} arquivo(s) seriam removidos`,
|
|
266
|
+
wouldClean: result.cleaned
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const result = await git.cleanUntracked(projectPath);
|
|
271
|
+
return asToolResult({
|
|
272
|
+
success: true,
|
|
273
|
+
...result,
|
|
274
|
+
message: result.cleaned.length > 0
|
|
275
|
+
? `${result.cleaned.length} arquivo(s) não rastreados removidos`
|
|
276
|
+
: "Nenhum arquivo para limpar"
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (action === "ensure-remotes") {
|
|
280
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
281
|
+
const isPublic = args.isPublic === true; // Default: privado
|
|
282
|
+
const organization = args.organization || undefined;
|
|
283
|
+
|
|
284
|
+
if (args.dryRun) {
|
|
285
|
+
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: false, isPublic, organization }); // Don't create for dry run
|
|
286
|
+
const urls = await pm.getRemoteUrls(repo, organization);
|
|
287
|
+
|
|
288
|
+
return asToolResult({
|
|
289
|
+
success: true,
|
|
290
|
+
dryRun: true,
|
|
291
|
+
message: "DRY RUN: Remotes seriam configurados" + (organization ? ` [org: ${organization}]` : ""),
|
|
292
|
+
repo,
|
|
293
|
+
organization: organization || undefined,
|
|
294
|
+
githubUrl: urls.github || "",
|
|
295
|
+
giteaUrl: urls.gitea || "",
|
|
296
|
+
ensured
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic, organization });
|
|
301
|
+
// Build URLs from actual ensureRepos results (handles GitHub fallback to personal account)
|
|
302
|
+
const githubUrl = ensured.github?.ok && ensured.github.repo
|
|
303
|
+
? `https://github.com/${ensured.github.repo}.git` : "";
|
|
304
|
+
const giteaBase = pm.giteaUrl?.replace(/\/$/, "");
|
|
305
|
+
const giteaUrl = ensured.gitea?.ok && ensured.gitea.repo && giteaBase
|
|
306
|
+
? `${giteaBase}/${ensured.gitea.repo}.git` : "";
|
|
307
|
+
await git.ensureRemotes(projectPath, { githubUrl, giteaUrl, organization });
|
|
308
|
+
const remotes = await git.listRemotes(projectPath);
|
|
309
|
+
return asToolResult({ success: true, ensured, remotes, isPrivate: !isPublic, organization: organization || undefined }, { tool: 'workflow', action: 'ensure-remotes' });
|
|
310
|
+
}
|
|
311
|
+
if (action === "push") {
|
|
312
|
+
const branch = await git.getCurrentBranch(projectPath);
|
|
313
|
+
const force = !!args.force;
|
|
314
|
+
|
|
315
|
+
if (args.dryRun) {
|
|
316
|
+
return asToolResult({
|
|
317
|
+
success: true,
|
|
318
|
+
dryRun: true,
|
|
319
|
+
message: `DRY RUN: Push seria executado na branch '${branch}'${force ? ' (force)' : ''}`,
|
|
320
|
+
branch,
|
|
321
|
+
force
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const organization = args.organization || undefined;
|
|
326
|
+
|
|
327
|
+
// Log push start
|
|
328
|
+
await sendLog(server, "info", "push iniciado", { branch, force, organization });
|
|
329
|
+
|
|
330
|
+
// Retry logic for push (often fails due to network or concurrent updates)
|
|
331
|
+
const result = await withRetry(
|
|
332
|
+
() => git.pushParallel(projectPath, branch, force, organization),
|
|
333
|
+
3,
|
|
334
|
+
"push"
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
return asToolResult({ success: true, branch, ...result });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (action === "init-branch") {
|
|
341
|
+
const branch = args.branch;
|
|
342
|
+
if (!branch) {
|
|
343
|
+
return asToolError("MISSING_PARAMETER", "Parâmetro 'branch' é obrigatório para action='init-branch'");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const channel = args.channel || "production";
|
|
347
|
+
const wtPath = path.join(projectPath, branch);
|
|
348
|
+
|
|
349
|
+
if (fs.existsSync(wtPath)) {
|
|
350
|
+
return asToolError("WORKTREE_PATH_EXISTS", `Diretório '${wtPath}' já existe.`, { suggestion: "Use git-worktree add se quiser configurar path customizado" });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
await git.addWorktree(projectPath, branch, wtPath);
|
|
355
|
+
await git.setWorktreeConfig(projectPath, branch, { path: wtPath, channel });
|
|
356
|
+
|
|
357
|
+
return asToolResult({
|
|
358
|
+
success: true,
|
|
359
|
+
branch,
|
|
360
|
+
path: wtPath,
|
|
361
|
+
channel,
|
|
362
|
+
message: `Worktree '${branch}' criado com sucesso no canal '${channel}'`
|
|
363
|
+
});
|
|
364
|
+
} catch (e) {
|
|
365
|
+
return errorToResponse(e);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ============ UPDATE - FLUXO AUTOMATIZADO COMPLETO ============
|
|
370
|
+
if (action === "update") {
|
|
371
|
+
if (!args.message) {
|
|
372
|
+
return asToolError("MISSING_PARAMETER", "message é obrigatório para update", { parameter: "message" });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const files = Array.isArray(args.files) && args.files.length ? args.files : ["."];
|
|
376
|
+
const force = !!args.force;
|
|
377
|
+
const skipIfClean = !!args.skipIfClean;
|
|
378
|
+
const gitignorePatterns = Array.isArray(args.gitignore) ? args.gitignore : [];
|
|
379
|
+
const syncBranches = !!args.syncBranches;
|
|
380
|
+
|
|
381
|
+
// 0. Gitignore (se solicitado)
|
|
382
|
+
let gitignored = [];
|
|
383
|
+
if (gitignorePatterns.length > 0) {
|
|
384
|
+
if (args.dryRun) {
|
|
385
|
+
gitignored = gitignorePatterns;
|
|
386
|
+
} else {
|
|
387
|
+
await git.addToGitignore(projectPath, gitignorePatterns);
|
|
388
|
+
gitignored = gitignorePatterns;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// 1. Status para ver o que mudou
|
|
393
|
+
const status = await git.status(projectPath);
|
|
394
|
+
|
|
395
|
+
const hasChanges = !status.isClean ||
|
|
396
|
+
(status.not_added?.length > 0) ||
|
|
397
|
+
(status.modified?.length > 0) ||
|
|
398
|
+
(status.created?.length > 0) ||
|
|
399
|
+
(status.deleted?.length > 0) ||
|
|
400
|
+
(status.staged?.length > 0);
|
|
401
|
+
|
|
402
|
+
if (!hasChanges && skipIfClean && gitignored.length === 0) {
|
|
403
|
+
return asToolResult({
|
|
404
|
+
success: true,
|
|
405
|
+
skipped: true,
|
|
406
|
+
reason: "Working tree limpa, nada para atualizar",
|
|
407
|
+
status
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (!hasChanges && gitignored.length === 0) {
|
|
412
|
+
return asToolResult({
|
|
413
|
+
success: false,
|
|
414
|
+
error: "NOTHING_TO_UPDATE",
|
|
415
|
+
message: "Nenhuma mudança para atualizar. Use skipIfClean=true para pular silenciosamente.",
|
|
416
|
+
status
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (args.dryRun) {
|
|
421
|
+
return asToolResult({
|
|
422
|
+
success: true,
|
|
423
|
+
dryRun: true,
|
|
424
|
+
message: "DRY RUN: Update seria executado",
|
|
425
|
+
wouldExecute: ["gitignore (opcional)", "status", "add", "commit", "push" + (syncBranches ? " (sync all worktrees)" : "")],
|
|
426
|
+
files,
|
|
427
|
+
commitMessage: args.message,
|
|
428
|
+
gitignored,
|
|
429
|
+
status,
|
|
430
|
+
syncBranches
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 2. Add
|
|
435
|
+
await git.add(projectPath, files);
|
|
436
|
+
|
|
437
|
+
// 3. Commit
|
|
438
|
+
const sha = await git.commit(projectPath, args.message);
|
|
439
|
+
|
|
440
|
+
// 3.5. Garantir remotes com organization (se fornecida)
|
|
441
|
+
const organization = args.organization || undefined;
|
|
442
|
+
if (organization) {
|
|
443
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
444
|
+
const isPublic = args.isPublic === true;
|
|
445
|
+
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic, organization });
|
|
446
|
+
// Build URLs from actual ensureRepos results (handles GitHub fallback to personal account)
|
|
447
|
+
const githubUrl = ensured.github?.ok && ensured.github.repo
|
|
448
|
+
? `https://github.com/${ensured.github.repo}.git` : "";
|
|
449
|
+
const giteaBase = pm.giteaUrl?.replace(/\/$/, "");
|
|
450
|
+
const giteaUrl = ensured.gitea?.ok && ensured.gitea.repo && giteaBase
|
|
451
|
+
? `${giteaBase}/${ensured.gitea.repo}.git` : "";
|
|
452
|
+
await git.ensureRemotes(projectPath, { githubUrl, giteaUrl, organization });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 4. Push Strategy
|
|
456
|
+
const branch = await git.getCurrentBranch(projectPath);
|
|
457
|
+
let pushResult = {};
|
|
458
|
+
let synced = [];
|
|
459
|
+
let errors = [];
|
|
460
|
+
|
|
461
|
+
// Determine Channel
|
|
462
|
+
let channel = args.channel;
|
|
463
|
+
if (!channel) {
|
|
464
|
+
const storedChannel = await git.getConfig(projectPath, `worktree-branch.${branch}.channel`);
|
|
465
|
+
if (storedChannel) channel = storedChannel;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Default channel
|
|
469
|
+
if (!channel) channel = "production";
|
|
470
|
+
|
|
471
|
+
// SYNC BRANCHES
|
|
472
|
+
if (syncBranches) {
|
|
473
|
+
// Só permitido no principal (onde .git é diretório)
|
|
474
|
+
if (fs.existsSync(path.join(projectPath, ".git")) && fs.statSync(path.join(projectPath, ".git")).isFile()) {
|
|
475
|
+
return asToolError("INVALID_OPERATION", "syncBranches=true só pode ser executado a partir do repositório principal, não de um worktree.");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 1. Push do principal (current)
|
|
479
|
+
const remoteBranch = resolveRemoteBranch(branch, channel);
|
|
480
|
+
const remotes = await git.listRemotes(projectPath);
|
|
481
|
+
|
|
482
|
+
await sendLog(server, "info", "update: push iniciado (sync)", { branch, remoteBranch, force });
|
|
483
|
+
|
|
484
|
+
const mainPushed = [];
|
|
485
|
+
const mainFailed = [];
|
|
486
|
+
for (const r of remotes) {
|
|
487
|
+
try {
|
|
488
|
+
await git.pushRefspec(projectPath, r.remote, branch, remoteBranch, force);
|
|
489
|
+
mainPushed.push(r.remote);
|
|
490
|
+
} catch(e) {
|
|
491
|
+
mainFailed.push({ remote: r.remote, error: e.message });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 2. Propagar para worktrees
|
|
496
|
+
// Req 8.3: synced.length + errors.length === N (número de worktrees registrados)
|
|
497
|
+
// O principal NÃO entra no contador — só os worktrees registrados
|
|
498
|
+
const configs = await git.getWorktreeConfigs(projectPath);
|
|
499
|
+
|
|
500
|
+
for (const wt of configs) {
|
|
501
|
+
// Pula se for a própria branch principal (caso esteja registrada)
|
|
502
|
+
if (wt.branch === branch) continue;
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
// Merge main -> worktree
|
|
506
|
+
await git.merge(wt.path, branch);
|
|
507
|
+
|
|
508
|
+
// Resolve remote branch do worktree
|
|
509
|
+
const wtRemoteBranch = resolveRemoteBranch(wt.branch, wt.channel);
|
|
510
|
+
|
|
511
|
+
// Push worktree
|
|
512
|
+
const wtRemotes = await git.listRemotes(wt.path);
|
|
513
|
+
for (const r of wtRemotes) {
|
|
514
|
+
await git.pushRefspec(wt.path, r.remote, wt.branch, wtRemoteBranch, false); // Nunca force no sync
|
|
515
|
+
}
|
|
516
|
+
synced.push({ branch: wt.branch, remoteBranch: wtRemoteBranch, channel: wt.channel });
|
|
517
|
+
} catch (e) {
|
|
518
|
+
errors.push({ branch: wt.branch, error: e.message });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
pushResult = {
|
|
523
|
+
main: { branch, remoteBranch, channel, pushed: mainPushed, failed: mainFailed },
|
|
524
|
+
synced,
|
|
525
|
+
errors
|
|
526
|
+
};
|
|
527
|
+
} else {
|
|
528
|
+
// NORMAL UPDATE (SINGLE BRANCH)
|
|
529
|
+
const remoteBranch = resolveRemoteBranch(branch, channel);
|
|
530
|
+
|
|
531
|
+
if (remoteBranch === branch) {
|
|
532
|
+
// Comportamento padrão (production)
|
|
533
|
+
await sendLog(server, "info", "update: push iniciado", { branch, force });
|
|
534
|
+
pushResult = await withRetry(
|
|
535
|
+
() => git.pushParallel(projectPath, branch, force, organization),
|
|
536
|
+
3,
|
|
537
|
+
"push"
|
|
538
|
+
);
|
|
539
|
+
} else {
|
|
540
|
+
// Comportamento customizado (beta/alpha) -> pushRefspec
|
|
541
|
+
const remotes = await git.listRemotes(projectPath);
|
|
542
|
+
|
|
543
|
+
const pushed = [];
|
|
544
|
+
const failed = [];
|
|
545
|
+
|
|
546
|
+
await sendLog(server, "info", "update: push iniciado (refspec)", { branch, remoteBranch, force });
|
|
547
|
+
|
|
548
|
+
for (const r of remotes) {
|
|
549
|
+
try {
|
|
550
|
+
await git.pushRefspec(projectPath, r.remote, branch, remoteBranch, force);
|
|
551
|
+
pushed.push(r.remote);
|
|
552
|
+
} catch (e) {
|
|
553
|
+
failed.push({ remote: r.remote, error: e.message });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (pushed.length === 0 && failed.length > 0) {
|
|
558
|
+
// Melhor retornar erro se TODOS falharem
|
|
559
|
+
throw createError("PUSH_REJECTED", { message: "Push falhou para todos os remotes customizados", errors: failed });
|
|
560
|
+
}
|
|
561
|
+
pushResult = { pushed, failed, remoteBranch };
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return asToolResult({
|
|
566
|
+
success: true,
|
|
567
|
+
action: "update",
|
|
568
|
+
steps: ["status", "add", "commit", "push"],
|
|
569
|
+
sha,
|
|
570
|
+
message: args.message,
|
|
571
|
+
branch,
|
|
572
|
+
filesAdded: files,
|
|
573
|
+
gitignored,
|
|
574
|
+
organization: organization || undefined,
|
|
575
|
+
push: pushResult,
|
|
576
|
+
_aiContext: {
|
|
577
|
+
completed: true,
|
|
578
|
+
message: "Ciclo completo: arquivos adicionados, commit criado e push realizado" + (organization ? ` [org: ${organization}]` : "")
|
|
579
|
+
}
|
|
580
|
+
}, { tool: 'workflow', action: 'update' });
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
|
|
584
|
+
availableActions: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean", "update", "init-branch"],
|
|
585
|
+
suggestion: "Use action='update' para fluxo completo automatizado"
|
|
586
|
+
});
|
|
587
|
+
} catch (e) {
|
|
588
|
+
return errorToResponse(e);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return {
|
|
593
|
+
name: "git-workflow",
|
|
594
|
+
description,
|
|
595
|
+
inputSchema,
|
|
596
|
+
handle,
|
|
597
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
|
|
598
|
+
};
|
|
599
|
+
}
|