@andrebuzeli/git-mcp 15.5.0 → 15.7.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andrebuzeli/git-mcp",
3
- "version": "15.5.0",
3
+ "version": "15.7.0",
4
4
  "private": false,
5
5
  "description": "MCP server para Git com operações locais e sincronização paralela GitHub/Gitea",
6
6
  "license": "MIT",
package/src/index.js CHANGED
@@ -24,6 +24,7 @@ import { createGitDiffTool } from "./tools/git-diff.js";
24
24
  import { createGitCloneTool } from "./tools/git-clone.js";
25
25
  import { createGitHelpTool } from "./tools/git-help.js";
26
26
  import { getResources, readResource } from "./resources/index.js";
27
+ import { createPromptsHandler, PROMPTS } from "./prompts/index.js";
27
28
 
28
29
  // Carrega variáveis de ambiente do arquivo .env (se existir)
29
30
  loadEnv();
@@ -46,11 +47,14 @@ if (!hasGitHub && !hasGitea) {
46
47
 
47
48
  const transport = new StdioServerTransport();
48
49
  const server = new Server(
49
- { name: "git-mcpv2", version: "15.4.0" },
50
- { capabilities: { tools: {}, resources: {} } }
50
+ { name: "git-mcpv2", version: "15.7.0" },
51
+ { capabilities: { tools: {}, resources: {}, prompts: {} } }
51
52
  );
52
53
  server.connect(transport);
53
54
 
55
+ // Prompts handler
56
+ const promptsHandler = createPromptsHandler(git, pm);
57
+
54
58
  // Ajv singleton para validação
55
59
  const ajv = new Ajv({ allErrors: true });
56
60
 
@@ -123,5 +127,20 @@ server.setRequestHandler(
123
127
  }
124
128
  );
125
129
 
130
+ // ============ PROMPTS ============
131
+ server.setRequestHandler(
132
+ (await import("@modelcontextprotocol/sdk/types.js")).ListPromptsRequestSchema,
133
+ async () => ({ prompts: PROMPTS })
134
+ );
135
+
136
+ server.setRequestHandler(
137
+ (await import("@modelcontextprotocol/sdk/types.js")).GetPromptRequestSchema,
138
+ async (req) => {
139
+ const name = req.params?.name || "";
140
+ const args = req.params?.arguments || {};
141
+ return await promptsHandler.get(name, args);
142
+ }
143
+ );
144
+
126
145
  // Keep process alive
127
146
  process.stdin.resume();
@@ -0,0 +1,828 @@
1
+ /**
2
+ * MCP Prompts para git-mcp
3
+ *
4
+ * Prompts são templates reutilizáveis que guiam interações com o LLM.
5
+ * Diferente de Tools (executam lógica), Prompts retornam mensagens formatadas.
6
+ * UTILIZANDO APENAS O GIT-MCP
7
+ */
8
+
9
+ // ============ PROMPT DEFINITIONS ============
10
+
11
+ export const PROMPTS = [
12
+ {
13
+ name: "git-status",
14
+ description: "Mostra status completo do repositório Git. UTILIZANDO APENAS O GIT-MCP"
15
+ },
16
+ {
17
+ name: "git-history",
18
+ description: "Exibe histórico de commits do repositório. UTILIZANDO APENAS O GIT-MCP"
19
+ },
20
+ {
21
+ name: "git-remote",
22
+ description: "Lista e mostra informações dos remotes configurados. UTILIZANDO APENAS O GIT-MCP"
23
+ },
24
+ {
25
+ name: "git-commit",
26
+ description: "Prepara e cria commit das mudanças (sem configurar email). UTILIZANDO APENAS O GIT-MCP"
27
+ },
28
+ {
29
+ name: "git-push",
30
+ description: "Envia commits locais para os remotes (GitHub/Gitea). UTILIZANDO APENAS O GIT-MCP"
31
+ },
32
+ {
33
+ name: "git-cleanup",
34
+ description: "Limpa arquivos não rastreados do repositório. UTILIZANDO APENAS O GIT-MCP"
35
+ },
36
+ {
37
+ name: "git-review",
38
+ description: "Faz code review das mudanças atuais do repositório. UTILIZANDO APENAS O GIT-MCP"
39
+ },
40
+ {
41
+ name: "git-change",
42
+ description: "Gera changelog das mudanças desde última tag ou versão específica. UTILIZANDO APENAS O GIT-MCP"
43
+ },
44
+ {
45
+ name: "git-lint",
46
+ description: "Analisa código modificado procurando problemas comuns e más práticas. UTILIZANDO APENAS O GIT-MCP"
47
+ },
48
+ {
49
+ name: "git-debug",
50
+ description: "Ajuda a encontrar bugs analisando histórico de commits. UTILIZANDO APENAS O GIT-MCP"
51
+ },
52
+ {
53
+ name: "git-release",
54
+ description: "Cria release completa do projeto com tag e release notes. UTILIZANDO APENAS O GIT-MCP"
55
+ },
56
+ {
57
+ name: "git-update",
58
+ description: "Atualiza projeto completo nos providers: commit, push e sincroniza GitHub/Gitea. UTILIZANDO APENAS O GIT-MCP"
59
+ }
60
+ ];
61
+
62
+ // ============ PROMPT TEMPLATES ============
63
+
64
+ const PROMPT_TEMPLATES = {
65
+ "git-status": {
66
+ getMessages: (context) => [{
67
+ role: "user",
68
+ content: {
69
+ type: "text",
70
+ text: `# Status do Repositório Git
71
+
72
+ Analise o status atual do repositório e forneça um resumo claro e acionável.
73
+
74
+ ## Status Atual
75
+
76
+ - **Branch**: ${context.branch || "N/A"}
77
+ - **Arquivos modificados**: ${context.modified?.length || 0}
78
+ - **Arquivos staged**: ${context.staged?.length || 0}
79
+ - **Arquivos não rastreados**: ${context.notAdded?.length || 0}
80
+ - **Arquivos deletados**: ${context.deleted?.length || 0}
81
+ - **Working tree limpa**: ${context.isClean ? "Sim" : "Não"}
82
+
83
+ ## Arquivos Modificados
84
+ ${context.modified?.map(f => `- ${f}`).join("\n") || "Nenhum"}
85
+
86
+ ## Arquivos Staged (prontos para commit)
87
+ ${context.staged?.map(f => `- ${f}`).join("\n") || "Nenhum"}
88
+
89
+ ## Arquivos Não Rastreados
90
+ ${context.notAdded?.map(f => `- ${f}`).join("\n") || "Nenhum"}
91
+
92
+ ## Instruções
93
+
94
+ Forneça um resumo do status atual e sugira próximos passos. UTILIZANDO APENAS AS TOOLS DO GIT-MCP:
95
+ - Use \`git-workflow\` para add, commit, push
96
+ - Use \`git-branches\` para gerenciar branches
97
+ - Use \`git-status\` apenas para consulta`
98
+ }
99
+ }]
100
+ },
101
+
102
+ "git-history": {
103
+ getMessages: (context) => {
104
+ const maxCount = context.maxCount || 20;
105
+ const userMessage = context.userMessage ? `\n\n## Observação do Usuário\n\n${context.userMessage}\n` : "";
106
+
107
+ return [{
108
+ role: "user",
109
+ content: {
110
+ type: "text",
111
+ text: `# Histórico de Commits${userMessage}
112
+
113
+ ## Branch Atual
114
+ ${context.branch || "N/A"}
115
+
116
+ ## Últimos ${context.commits?.length || 0} Commits
117
+
118
+ ${context.commits?.map(c => {
119
+ const author = typeof c.author === 'object' ? c.author?.name : c.author || "Unknown";
120
+ const date = c.date ? new Date(c.date).toLocaleString('pt-BR') : "N/A";
121
+ return `### ${c.shortSha || c.sha?.substring(0, 7) || "N/A"}
122
+ - **Autor**: ${author}
123
+ - **Data**: ${date}
124
+ - **Mensagem**: ${c.message || "Sem mensagem"}
125
+ - **SHA completo**: ${c.sha || "N/A"}`;
126
+ }).join("\n\n") || "Nenhum commit encontrado"}
127
+
128
+ ## Instruções
129
+
130
+ Apresente o histórico de commits de forma organizada. UTILIZANDO APENAS AS TOOLS DO GIT-MCP:
131
+ - Use \`git-history log\` para ver mais commits
132
+ - Use \`git-diff compare\` para comparar commits
133
+ - Use \`git-files read\` para ver arquivos em commits específicos`
134
+ }
135
+ }];
136
+ }
137
+ },
138
+
139
+ "git-remote": {
140
+ getMessages: (context) => [{
141
+ role: "user",
142
+ content: {
143
+ type: "text",
144
+ text: `# Remotes Configurados
145
+
146
+ ## Remotes Disponíveis
147
+
148
+ ${context.remotes?.map(r => `### ${r.name || r.remote || "N/A"}
149
+ - **URL**: ${r.url || "N/A"}
150
+ - **Tipo**: ${r.url?.includes("github.com") ? "GitHub" : r.url?.includes("gitea") ? "Gitea" : "Desconhecido"}
151
+ `).join("\n") || "Nenhum remote configurado"}
152
+
153
+ ## Status dos Providers
154
+
155
+ - **GitHub configurado**: ${context.hasGitHub ? "Sim" : "Não"}
156
+ - **Gitea configurado**: ${context.hasGitea ? "Sim" : "Não"}
157
+
158
+ ## Instruções
159
+
160
+ Apresente os remotes configurados e sugira configuração se necessário. UTILIZANDO APENAS AS TOOLS DO GIT-MCP:
161
+ - Use \`git-remote list\` para ver remotes
162
+ - Use \`git-remote ensure\` para configurar remotes automaticamente
163
+ - Use \`git-workflow ensure-remotes\` como alternativa`
164
+ }
165
+ }]
166
+ },
167
+
168
+ "git-commit": {
169
+ getMessages: (context) => {
170
+ const userMessage = context.userMessage || "";
171
+ const suggestedMessage = userMessage || context.suggestedMessage || "";
172
+
173
+ return [{
174
+ role: "user",
175
+ content: {
176
+ type: "text",
177
+ text: `# Criar Commit${userMessage ? `\n\n## Mensagem do Usuário\n\n${userMessage}\n` : ""}
178
+
179
+ ## Arquivos Staged (prontos para commit)
180
+ ${context.staged?.map(f => `- ${f}`).join("\n") || "Nenhum arquivo staged"}
181
+
182
+ ## Mudanças Staged
183
+ \`\`\`diff
184
+ ${context.stagedDiff || "Use git-diff para ver mudanças"}
185
+ \`\`\`
186
+
187
+ ## Status Atual
188
+ - **Branch**: ${context.branch || "N/A"}
189
+ - **Arquivos staged**: ${context.staged?.length || 0}
190
+ - **Arquivos modificados (não staged)**: ${context.modified?.length || 0}
191
+
192
+ ## Instruções
193
+
194
+ ${context.staged?.length > 0
195
+ ? `Crie um commit com as mudanças staged. ${userMessage ? `Use a mensagem fornecida pelo usuário: "${userMessage}"` : "Se não houver mensagem do usuário, sugira uma mensagem descritiva baseada nas mudanças."}
196
+
197
+ UTILIZANDO APENAS AS TOOLS DO GIT-MCP:
198
+ - Use \`git-workflow commit\` com a mensagem apropriada
199
+ - NÃO configure email (o usuário não quer isso)
200
+ - Se houver arquivos modificados não staged, sugira usar \`git-workflow add\` primeiro`
201
+ : `Não há arquivos staged para commit. Execute primeiro: \`git-workflow add\` com os arquivos desejados.
202
+
203
+ UTILIZANDO APENAS AS TOOLS DO GIT-MCP:
204
+ - Use \`git-workflow add\` para adicionar arquivos ao staging
205
+ - Depois use \`git-workflow commit\` para criar o commit`}`
206
+ }
207
+ }];
208
+ }
209
+ },
210
+
211
+ "git-push": {
212
+ getMessages: (context) => {
213
+ const forceFlag = context.userMessage?.toLowerCase().includes("force") ? true : false;
214
+
215
+ return [{
216
+ role: "user",
217
+ content: {
218
+ type: "text",
219
+ text: `# Push para Remotes
220
+
221
+ ## Status Atual
222
+ - **Branch atual**: ${context.branch || "N/A"}
223
+ - **Commits locais não enviados**: ${context.unpushedCommits || "Desconhecido"}
224
+ - **Remotes configurados**: ${context.remotes?.map(r => r.name).join(", ") || "Nenhum"}
225
+ - **Force push necessário?**: ${forceFlag ? "Sim (solicitado pelo usuário)" : "Não"}
226
+
227
+ ## Remotes Disponíveis
228
+ ${context.remotes?.map(r => `- **${r.name}**: ${r.url}`).join("\n") || "Nenhum"}
229
+
230
+ ## Instruções
231
+
232
+ Envie os commits locais para os remotes (GitHub/Gitea). UTILIZANDO APENAS AS TOOLS DO GIT-MCP:
233
+ - Use \`git-workflow push\` para enviar para todos os remotes em paralelo
234
+ - ${forceFlag ? "Use force=true se necessário (foi solicitado)" : "Use force=true apenas se push for rejeitado"}
235
+ - Se não houver remotes configurados, use \`git-remote ensure\` primeiro
236
+ - Se não houver commits para enviar, informe o usuário`
237
+ }
238
+ }];
239
+ }
240
+ },
241
+
242
+ "git-cleanup": {
243
+ getMessages: (context) => [{
244
+ role: "user",
245
+ content: {
246
+ type: "text",
247
+ text: `# Limpar Arquivos Não Rastreados
248
+
249
+ ## Arquivos Não Rastreados Detectados
250
+ ${context.untrackedFiles?.map(f => `- ${f}`).join("\n") || "Nenhum arquivo não rastreado encontrado"}
251
+
252
+ ## Total de Arquivos para Limpar
253
+ ${context.untrackedFiles?.length || 0}
254
+
255
+ ## Instruções
256
+
257
+ ${context.untrackedFiles?.length > 0
258
+ ? `Limpe os arquivos não rastreados do repositório. UTILIZANDO APENAS AS TOOLS DO GIT-MCP:
259
+ - Use \`git-workflow clean\` para remover arquivos não rastreados
260
+ - Antes de limpar, certifique-se de que não são arquivos importantes
261
+ - Liste os arquivos que serão removidos antes de confirmar`
262
+ : `Não há arquivos não rastreados para limpar. O repositório está limpo.`}`
263
+ }
264
+ }]
265
+ },
266
+
267
+ "git-review": {
268
+ getMessages: (context) => {
269
+ const userMessage = context.userMessage ? `\n\n## Observação do Usuário\n\n${context.userMessage}\n` : "";
270
+
271
+ return [{
272
+ role: "user",
273
+ content: {
274
+ type: "text",
275
+ text: `# Code Review Request${userMessage}
276
+
277
+ Você é um code reviewer experiente. Analise as mudanças abaixo e forneça um review detalhado.
278
+
279
+ ## Mudanças para Review
280
+
281
+ \`\`\`diff
282
+ ${context.diff || "Nenhuma mudança detectada"}
283
+ \`\`\`
284
+
285
+ ## Arquivos Modificados
286
+ ${context.files?.map(f => `- ${f}`).join("\n") || "Nenhum arquivo modificado"}
287
+
288
+ ## Instruções de Review
289
+
290
+ Por favor, analise:
291
+
292
+ 1. **🐛 Bugs Potenciais**: Identifique possíveis bugs ou erros lógicos
293
+ 2. **🔒 Segurança**: Verifique vulnerabilidades de segurança
294
+ 3. **⚡ Performance**: Aponte problemas de performance
295
+ 4. **📖 Legibilidade**: Sugira melhorias de código limpo
296
+ 5. **🧪 Testabilidade**: Comente sobre cobertura de testes
297
+ 6. **✅ Pontos Positivos**: Destaque o que está bem feito
298
+
299
+ Formate sua resposta de forma clara e acionável. UTILIZANDO APENAS AS TOOLS DO GIT-MCP para coletar informações adicionais se necessário.`
300
+ }
301
+ }];
302
+ }
303
+ },
304
+
305
+ "git-change": {
306
+ getMessages: (context) => {
307
+ const sinceTag = context.userMessage || context.lastTag || null;
308
+
309
+ return [{
310
+ role: "user",
311
+ content: {
312
+ type: "text",
313
+ text: `# Gerar Changelog
314
+
315
+ ## Informações da Versão
316
+
317
+ - **Última tag**: ${context.lastTag || "Nenhuma tag encontrada"}
318
+ - **Desde**: ${sinceTag || "última tag"}
319
+ - **Branch**: ${context.branch || "N/A"}
320
+ - **Commits desde última tag**: ${context.commits?.length || 0}
321
+
322
+ ## Commits para Changelog
323
+
324
+ ${context.commits?.map(c => {
325
+ const message = c.message || "Sem mensagem";
326
+ const sha = c.shortSha || c.sha?.substring(0, 7) || "N/A";
327
+ return `- \`${sha}\`: ${message}`;
328
+ }).join("\n") || "Nenhum commit encontrado"}
329
+
330
+ ## Instruções
331
+
332
+ Gere um changelog formatado baseado nos commits desde ${sinceTag || "a última tag"}. Organize por categorias:
333
+ - ✨ **Features** (feat:, feature:)
334
+ - 🐛 **Bug Fixes** (fix:, bug:)
335
+ - 📝 **Documentation** (docs:)
336
+ - 🔧 **Refactoring** (refactor:)
337
+ - ⚡ **Performance** (perf:)
338
+ - 🧪 **Tests** (test:)
339
+ - 🔒 **Security** (security:)
340
+ - 💥 **Breaking Changes** (BREAKING:)
341
+
342
+ UTILIZANDO APENAS AS TOOLS DO GIT-MCP para coletar mais informações se necessário.`
343
+ }
344
+ }];
345
+ }
346
+ },
347
+
348
+ "git-lint": {
349
+ getMessages: (context) => [{
350
+ role: "user",
351
+ content: {
352
+ type: "text",
353
+ text: `# Análise de Código (Lint)
354
+
355
+ ## Arquivos para Analisar
356
+ ${context.modifiedFiles?.map(f => `- ${f}`).join("\n") || "Nenhum arquivo modificado"}
357
+
358
+ ## Mudanças nos Arquivos
359
+
360
+ ${context.fileContents?.map(({ file, content }) => {
361
+ const preview = content ? content.substring(0, 500) + (content.length > 500 ? "..." : "") : "Arquivo vazio ou não acessível";
362
+ return `### ${file}
363
+ \`\`\`
364
+ ${preview}
365
+ \`\`\``;
366
+ }).join("\n\n") || "Nenhum conteúdo disponível"}
367
+
368
+ ## Instruções
369
+
370
+ Analise o código modificado procurando:
371
+ 1. **Problemas comuns**: variáveis não usadas, imports desnecessários, código morto
372
+ 2. **Más práticas**: funções muito longas, complexidade ciclomática alta, código duplicado
373
+ 3. **Potenciais bugs**: uso incorreto de APIs, condições sempre verdadeiras/falsas
374
+ 4. **Performance**: loops ineficientes, operações custosas desnecessárias
375
+ 5. **Segurança**: uso inseguro de eval, concatenação SQL, XSS potencial
376
+ 6. **Padrões**: inconsistências de estilo, convenções de nomenclatura
377
+
378
+ Forneça sugestões específicas e acionáveis. UTILIZANDO APENAS AS TOOLS DO GIT-MCP para coletar mais informações se necessário.`
379
+ }
380
+ }]
381
+ },
382
+
383
+ "git-debug": {
384
+ getMessages: (context) => {
385
+ const bugDescription = context.userMessage || "Qual bug você está procurando?";
386
+ const userContext = context.userMessage ? `\n\n## Descrição do Bug\n\n${bugDescription}\n` : `\n\n${bugDescription}`;
387
+
388
+ return [{
389
+ role: "user",
390
+ content: {
391
+ type: "text",
392
+ text: `# Encontrar Bug no Histórico Git${userContext}
393
+
394
+ Você é um detetive de bugs. Use o histórico Git para encontrar quando/onde um bug foi introduzido.
395
+
396
+ ## Informações do Repositório
397
+
398
+ - **Branch**: ${context.branch || "N/A"}
399
+ - **Total de commits**: ${context.totalCommits || "N/A"}
400
+
401
+ ## Últimos Commits
402
+ ${context.recentCommits?.map(c => {
403
+ const author = typeof c.author === 'object' ? c.author?.name : c.author || "Unknown";
404
+ return `- ${c.shortSha} | ${c.date} | ${author}: ${c.message}`;
405
+ }).join("\n") || "Use git-history para ver commits"}
406
+
407
+ ## Estratégia de Debug
408
+
409
+ 1. **Identificar sintoma**: Descreva o bug que está procurando
410
+ 2. **Usar git-history**: Veja commits recentes
411
+ 3. **Analisar mudanças**: Compare commits suspeitos
412
+ 4. **Bisect manual**:
413
+ - Identifique um commit "bom" (sem bug)
414
+ - Identifique um commit "ruim" (com bug)
415
+ - Analise commits no meio para encontrar o culpado
416
+ 5. **Verificar arquivo específico**: Leia arquivos em commits diferentes
417
+
418
+ ## Ferramentas Disponíveis
419
+
420
+ UTILIZANDO APENAS AS TOOLS DO GIT-MCP:
421
+ - \`git-history log\`: Ver histórico de commits
422
+ - \`git-diff compare\`: Comparar entre commits/branches
423
+ - \`git-files read\`: Ler arquivo em commit específico
424
+ - \`git-diff show\`: Ver mudanças atuais
425
+
426
+ Analise o histórico e sugira qual commit pode ter introduzido o bug.`
427
+ }
428
+ }];
429
+ }
430
+ },
431
+
432
+ "git-release": {
433
+ getMessages: (context) => [{
434
+ role: "user",
435
+ content: {
436
+ type: "text",
437
+ text: `# Criar Release do Projeto
438
+
439
+ Você vai criar uma nova release deste projeto.
440
+
441
+ ## Informações Atuais
442
+
443
+ - **Última tag**: ${context.lastTag || "Nenhuma tag encontrada"}
444
+ - **Branch**: ${context.branch || "N/A"}
445
+ - **Commits desde última tag**: ${context.commitsSinceTag || context.commits?.length || "N/A"}
446
+
447
+ ## Commits para esta Release
448
+ ${context.recentCommits?.map(c => `- ${c.shortSha}: ${c.message}`).join("\n") || "Use git-history para ver commits"}
449
+
450
+ ## Passos para Release
451
+
452
+ 1. **Determinar versão**: Baseado nos commits, sugira a próxima versão (semver)
453
+ - MAJOR (x.0.0): Breaking changes
454
+ - MINOR (0.x.0): Novas features
455
+ - PATCH (0.0.x): Bug fixes
456
+
457
+ 2. **Criar Tag**: Use \`git-tags create\` com:
458
+ - tag: "vX.Y.Z"
459
+ - message: Descrição da release
460
+
461
+ 3. **Push Tag**: Use \`git-tags push\`
462
+
463
+ 4. **Criar Release**: Use \`git-remote release-create\` com:
464
+ - tag: "vX.Y.Z"
465
+ - name: "Release vX.Y.Z"
466
+ - body: Release notes geradas
467
+
468
+ ## Template de Release Notes
469
+
470
+ \`\`\`markdown
471
+ ## 🚀 Release vX.Y.Z
472
+
473
+ ### ✨ Novidades
474
+ - Feature 1
475
+ - Feature 2
476
+
477
+ ### 🐛 Correções
478
+ - Fix 1
479
+ - Fix 2
480
+
481
+ ### 📝 Outras Mudanças
482
+ - Mudança 1
483
+ \`\`\`
484
+
485
+ Analise os commits e execute os passos para criar a release. UTILIZANDO APENAS AS TOOLS DO GIT-MCP.`
486
+ }
487
+ }]
488
+ },
489
+
490
+ "git-update": {
491
+ getMessages: (context) => [{
492
+ role: "user",
493
+ content: {
494
+ type: "text",
495
+ text: `# Atualizar Projeto nos Providers
496
+
497
+ Você precisa atualizar este projeto Git nos providers remotos (GitHub/Gitea).
498
+
499
+ ## Status Atual do Repositório
500
+
501
+ - **É repositório Git?**: ${context.isGitRepo ? "Sim" : "Não"}
502
+ - **Branch atual**: ${context.branch || "N/A"}
503
+ - **Arquivos modificados**: ${context.modified?.length || 0}
504
+ - **Arquivos staged**: ${context.staged?.length || 0}
505
+ - **Commits não enviados**: ${context.unpushedCommits || "N/A"}
506
+ - **Remotes configurados**: ${context.remotes?.join(", ") || "Nenhum"}
507
+
508
+ ## Arquivos Modificados
509
+ ${context.modified?.map(f => `- ${f}`).join("\n") || "Nenhum"}
510
+
511
+ ## Instruções
512
+
513
+ Atualize o projeto completo nos providers (GitHub e Gitea). Execute na ordem:
514
+
515
+ 1. **Se não é Git repo**: Use \`git-workflow init\` para inicializar
516
+ 2. **Se há arquivos modificados**: Use \`git-workflow add\` com files=["."]
517
+ 3. **Se há arquivos staged**: Use \`git-workflow commit\` com mensagem descritiva baseada nas mudanças
518
+ 4. **Se não há remotes**: Use \`git-remote ensure\` ou \`git-workflow ensure-remotes\` para configurar GitHub e Gitea
519
+ 5. **Se há commits locais**: Use \`git-workflow push\` para enviar para ambos os providers em paralelo
520
+
521
+ UTILIZANDO APENAS AS TOOLS DO GIT-MCP. Execute todas as ferramentas git-* na ordem correta para sincronizar completamente o projeto nos dois providers.`
522
+ }
523
+ }]
524
+ }
525
+ };
526
+
527
+ // ============ PROMPT HANDLER ============
528
+
529
+ /**
530
+ * Cria o handler de prompts para o MCP server
531
+ * @param {GitAdapter} git - Instância do GitAdapter
532
+ * @param {ProviderManager} pm - Instância do ProviderManager
533
+ */
534
+ export function createPromptsHandler(git, pm) {
535
+ return {
536
+ list: async () => PROMPTS,
537
+
538
+ get: async (name, args = {}) => {
539
+ const template = PROMPT_TEMPLATES[name];
540
+ if (!template) {
541
+ return {
542
+ description: `Prompt não encontrado: ${name}`,
543
+ messages: [{
544
+ role: "user",
545
+ content: { type: "text", text: `Erro: Prompt "${name}" não existe.` }
546
+ }]
547
+ };
548
+ }
549
+
550
+ // Coletar contexto baseado no prompt
551
+ const context = await gatherContext(name, args, git, pm);
552
+
553
+ return {
554
+ description: PROMPTS.find(p => p.name === name)?.description || name,
555
+ messages: template.getMessages(context)
556
+ };
557
+ }
558
+ };
559
+ }
560
+
561
+ /**
562
+ * Coleta contexto necessário para cada prompt
563
+ */
564
+ async function gatherContext(promptName, args, git, pm) {
565
+ const projectPath = args.projectPath || process.cwd();
566
+ const context = {
567
+ projectPath,
568
+ // Mensagem opcional do usuário (pode vir como message, userMessage, text, input, ou no final da string)
569
+ userMessage: args.message || args.userMessage || args.text || args.input || args.query || null
570
+ };
571
+
572
+ try {
573
+ switch (promptName) {
574
+ case "git-status": {
575
+ const status = await git.status(projectPath).catch(() => null);
576
+ if (status) {
577
+ context.branch = status.currentBranch || status.current;
578
+ context.modified = status.modified || [];
579
+ context.staged = status.staged || [];
580
+ context.notAdded = status.notAdded || status.not_added || [];
581
+ context.deleted = status.deleted || [];
582
+ context.isClean = status.isClean || false;
583
+ }
584
+ break;
585
+ }
586
+
587
+ case "git-history": {
588
+ const branch = await git.getCurrentBranch(projectPath).catch(() => null);
589
+ const maxCount = parseInt(context.userMessage) || 20; // Se userMessage é número, usa como maxCount
590
+ const commits = await git.log(projectPath, { ref: "HEAD", maxCount }).catch(() => []);
591
+
592
+ context.branch = branch;
593
+ context.commits = commits.map(c => ({
594
+ sha: c.sha,
595
+ shortSha: c.sha?.substring(0, 7),
596
+ message: c.message,
597
+ author: c.author,
598
+ date: c.date
599
+ }));
600
+ context.maxCount = maxCount;
601
+ // Se userMessage não é número, mantém como mensagem
602
+ if (isNaN(parseInt(context.userMessage))) {
603
+ // Mantém userMessage original
604
+ } else {
605
+ context.userMessage = null; // Remove se era número
606
+ }
607
+ break;
608
+ }
609
+
610
+ case "git-remote": {
611
+ const remotes = await git.listRemotes(projectPath).catch(() => []);
612
+
613
+ context.remotes = remotes;
614
+ context.hasGitHub = !!pm.github;
615
+ context.hasGitea = !!(pm.giteaUrl && pm.giteaToken);
616
+ break;
617
+ }
618
+
619
+ case "git-commit": {
620
+ const status = await git.status(projectPath).catch(() => null);
621
+ const branch = await git.getCurrentBranch(projectPath).catch(() => null);
622
+
623
+ context.branch = branch;
624
+ context.staged = status?.staged || [];
625
+ context.modified = status?.modified || [];
626
+
627
+ // Se há arquivos staged, pega diff deles
628
+ if (context.staged.length > 0) {
629
+ try {
630
+ // Tenta usar diff normal (mostra mudanças staged se houver)
631
+ context.stagedDiff = await git.diff(projectPath).catch(() => "");
632
+ if (!context.stagedDiff || context.stagedDiff.trim() === "") {
633
+ context.stagedDiff = `Arquivos staged: ${context.staged.join(", ")}. Use git-diff para ver mudanças detalhadas.`;
634
+ }
635
+ } catch {
636
+ context.stagedDiff = `Arquivos staged: ${context.staged.join(", ")}`;
637
+ }
638
+ }
639
+
640
+ // Sugere mensagem baseada nas mudanças
641
+ if (!context.userMessage && context.staged.length > 0) {
642
+ const fileNames = context.staged.slice(0, 3).join(", ");
643
+ context.suggestedMessage = `Update ${fileNames}${context.staged.length > 3 ? " and more" : ""}`;
644
+ }
645
+ break;
646
+ }
647
+
648
+ case "git-push": {
649
+ const branch = await git.getCurrentBranch(projectPath).catch(() => null);
650
+ const remotes = await git.listRemotes(projectPath).catch(() => []);
651
+
652
+ context.branch = branch;
653
+ context.remotes = remotes;
654
+ // Tenta detectar commits não enviados (comparando local com remote)
655
+ try {
656
+ const localCommits = await git.log(projectPath, { ref: "HEAD", maxCount: 10 }).catch(() => []);
657
+ context.unpushedCommits = localCommits.length > 0 ? localCommits.length : "Desconhecido";
658
+ } catch {
659
+ context.unpushedCommits = "Desconhecido";
660
+ }
661
+ break;
662
+ }
663
+
664
+ case "git-cleanup": {
665
+ const untracked = await git.cleanUntracked(projectPath).catch(() => ({ cleaned: [] }));
666
+ context.untrackedFiles = untracked.cleaned || [];
667
+ break;
668
+ }
669
+
670
+ case "git-review": {
671
+ const status = await git.status(projectPath).catch(() => null);
672
+ const diff = await git.diff(projectPath).catch(() => "");
673
+
674
+ context.diff = diff;
675
+ context.files = status ? [...(status.modified || []), ...(status.created || []), ...(status.deleted || [])] : [];
676
+ break;
677
+ }
678
+
679
+ case "git-change": {
680
+ const branch = await git.getCurrentBranch(projectPath).catch(() => null);
681
+ const tags = await git.listTags(projectPath).catch(() => []);
682
+
683
+ context.branch = branch;
684
+ context.lastTag = tags.length > 0 ? tags[tags.length - 1] : null; // Última tag (mais recente)
685
+
686
+ // Se userMessage é uma tag, usa ela, senão usa lastTag
687
+ const sinceTag = context.userMessage || context.lastTag;
688
+
689
+ // Pega commits desde a tag ou todos se não há tag
690
+ let commits = [];
691
+ if (sinceTag && context.userMessage) {
692
+ // Se usuário especificou uma tag/versão
693
+ try {
694
+ commits = await git.log(projectPath, { ref: sinceTag + "..HEAD", maxCount: 100 }).catch(() => []);
695
+ } catch {
696
+ // Se falhar, pega todos os commits recentes
697
+ commits = await git.log(projectPath, { ref: "HEAD", maxCount: 50 }).catch(() => []);
698
+ }
699
+ } else if (context.lastTag) {
700
+ // Usa última tag se disponível
701
+ try {
702
+ commits = await git.log(projectPath, { ref: context.lastTag + "..HEAD", maxCount: 100 }).catch(() => []);
703
+ } catch {
704
+ commits = await git.log(projectPath, { ref: "HEAD", maxCount: 50 }).catch(() => []);
705
+ }
706
+ } else {
707
+ // Sem tag, pega commits recentes
708
+ commits = await git.log(projectPath, { ref: "HEAD", maxCount: 50 }).catch(() => []);
709
+ }
710
+
711
+ context.commits = commits.map(c => ({
712
+ sha: c.sha,
713
+ shortSha: c.sha?.substring(0, 7),
714
+ message: c.message
715
+ }));
716
+ break;
717
+ }
718
+
719
+ case "git-lint": {
720
+ const status = await git.status(projectPath).catch(() => null);
721
+ const modifiedFiles = status ? [...(status.modified || []), ...(status.created || [])] : [];
722
+
723
+ context.modifiedFiles = modifiedFiles;
724
+
725
+ // Tenta ler conteúdo dos arquivos modificados (limitado a 5 para não sobrecarregar)
726
+ context.fileContents = [];
727
+ for (const file of modifiedFiles.slice(0, 5)) {
728
+ try {
729
+ const content = await git.readFile(projectPath, file, "HEAD").catch(() => null);
730
+ if (content) {
731
+ context.fileContents.push({ file, content });
732
+ }
733
+ } catch {
734
+ // Ignora erros ao ler arquivo
735
+ }
736
+ }
737
+ break;
738
+ }
739
+
740
+ case "git-debug": {
741
+ const branch = await git.getCurrentBranch(projectPath).catch(() => null);
742
+ const commits = await git.log(projectPath, { ref: "HEAD", maxCount: 20 }).catch(() => []);
743
+
744
+ context.branch = branch;
745
+ context.totalCommits = commits.length;
746
+ context.recentCommits = commits.map(c => ({
747
+ sha: c.sha,
748
+ shortSha: c.sha?.substring(0, 7),
749
+ message: c.message,
750
+ author: typeof c.author === 'object' ? c.author?.name : c.author || "Unknown",
751
+ date: c.date ? new Date(c.date).toLocaleString('pt-BR') : "N/A"
752
+ }));
753
+ break;
754
+ }
755
+
756
+ case "git-release": {
757
+ const branch = await git.getCurrentBranch(projectPath).catch(() => null);
758
+ const tags = await git.listTags(projectPath).catch(() => []);
759
+ const commits = await git.log(projectPath, { ref: "HEAD", maxCount: 50 }).catch(() => []);
760
+
761
+ context.branch = branch;
762
+ context.lastTag = tags.length > 0 ? tags[tags.length - 1] : null;
763
+
764
+ // Commits desde última tag
765
+ if (context.lastTag) {
766
+ try {
767
+ // Tenta pegar commits desde a última tag usando range
768
+ const commitsSinceTag = await git.log(projectPath, { ref: context.lastTag + "..HEAD", maxCount: 50 }).catch(() => []);
769
+ context.commitsSinceTag = commitsSinceTag.length;
770
+ context.recentCommits = commitsSinceTag.slice(0, 20).map(c => ({
771
+ sha: c.sha,
772
+ shortSha: c.sha?.substring(0, 7),
773
+ message: c.message
774
+ }));
775
+ } catch {
776
+ // Se falhar, usa commits recentes como fallback
777
+ context.commitsSinceTag = commits.length;
778
+ context.recentCommits = commits.slice(0, 20).map(c => ({
779
+ sha: c.sha,
780
+ shortSha: c.sha?.substring(0, 7),
781
+ message: c.message
782
+ }));
783
+ }
784
+ } else {
785
+ // Sem tag, usa commits recentes
786
+ context.commitsSinceTag = commits.length;
787
+ context.recentCommits = commits.slice(0, 20).map(c => ({
788
+ sha: c.sha,
789
+ shortSha: c.sha?.substring(0, 7),
790
+ message: c.message
791
+ }));
792
+ }
793
+ break;
794
+ }
795
+
796
+ case "git-update": {
797
+ const isGitRepo = await git.isRepo(projectPath).catch(() => false);
798
+ context.isGitRepo = isGitRepo;
799
+
800
+ if (isGitRepo) {
801
+ const status = await git.status(projectPath).catch(() => ({}));
802
+ const branch = await git.getCurrentBranch(projectPath).catch(() => null);
803
+ const remotes = await git.listRemotes(projectPath).catch(() => []);
804
+
805
+ context.branch = branch;
806
+ context.modified = status.modified || [];
807
+ context.staged = status.staged || [];
808
+ context.remotes = remotes.map(r => r.name || r.remote);
809
+
810
+ // Tenta detectar commits não enviados
811
+ try {
812
+ const localCommits = await git.log(projectPath, { ref: "HEAD", maxCount: 10 }).catch(() => []);
813
+ context.unpushedCommits = localCommits.length;
814
+ } catch {
815
+ context.unpushedCommits = "Desconhecido";
816
+ }
817
+ }
818
+ break;
819
+ }
820
+ }
821
+ } catch (e) {
822
+ context.error = e.message;
823
+ console.error(`[prompts] Error gathering context for ${promptName}:`, e);
824
+ }
825
+
826
+ return context;
827
+ }
828
+
@@ -14,7 +14,7 @@ export function createGitWorkflowTool(pm, git) {
14
14
  },
15
15
  action: {
16
16
  type: "string",
17
- enum: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean"],
17
+ enum: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean", "update"],
18
18
  description: `Ação a executar:
19
19
  - init: Inicializa repositório git local E cria repos no GitHub/Gitea automaticamente
20
20
  - status: Retorna arquivos modificados, staged, untracked (use ANTES de commit para ver o que mudou)
@@ -23,7 +23,8 @@ export function createGitWorkflowTool(pm, git) {
23
23
  - commit: Cria commit com os arquivos staged (use DEPOIS de add)
24
24
  - push: Envia commits para GitHub E Gitea em paralelo (use DEPOIS de commit)
25
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`
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`
27
28
  },
28
29
  files: {
29
30
  type: "array",
@@ -32,7 +33,7 @@ export function createGitWorkflowTool(pm, git) {
32
33
  },
33
34
  message: {
34
35
  type: "string",
35
- description: "Mensagem do commit. Obrigatório para action='commit'. Ex: 'feat: adiciona nova funcionalidade'"
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'"
36
37
  },
37
38
  force: {
38
39
  type: "boolean",
@@ -71,6 +72,7 @@ QUANDO USAR CADA ACTION:
71
72
  - init: Apenas uma vez, para novos projetos (cria .gitignore automaticamente)
72
73
  - ensure-remotes: Se push falhar por falta de configuração
73
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
74
76
 
75
77
  EXEMPLOS DE USO:
76
78
  • Iniciar projeto: { "projectPath": "/path/to/project", "action": "init" }
@@ -285,8 +287,106 @@ EXEMPLOS DE USO:
285
287
  const result = await git.pushParallel(projectPath, branch, force);
286
288
  return asToolResult({ success: true, branch, ...result });
287
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
+ }
288
388
  return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
289
- availableActions: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes"],
389
+ availableActions: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean", "update"],
290
390
  suggestion: "Use uma das actions disponíveis"
291
391
  });
292
392
  } catch (e) {
@@ -166,6 +166,15 @@ export class GitAdapter {
166
166
  }
167
167
  }
168
168
 
169
+ /**
170
+ * Verifica se o diretório é um repositório git
171
+ * @param {string} dir - Diretório para verificar
172
+ * @returns {boolean} - true se é um repo git
173
+ */
174
+ async isRepo(dir) {
175
+ return fs.existsSync(path.join(dir, ".git"));
176
+ }
177
+
169
178
  /**
170
179
  * Verifica integridade do repositório git
171
180
  * @param {string} dir - Diretório do repositório
@@ -385,7 +394,7 @@ export class GitAdapter {
385
394
  return null;
386
395
  }
387
396
 
388
- async pushOne(dir, remote, branch, force = false) {
397
+ async pushOne(dir, remote, branch, force = false, setUpstream = false) {
389
398
  const remoteUrl = await this._exec(dir, ["remote", "get-url", remote]);
390
399
  const header = this._getAuthHeader(remoteUrl);
391
400
 
@@ -395,13 +404,32 @@ export class GitAdapter {
395
404
  }
396
405
  args.push("push");
397
406
  if (force) args.push("--force");
398
- args.push(remote, branch);
407
+ if (setUpstream) {
408
+ args.push("-u", remote, branch);
409
+ } else {
410
+ args.push(remote, branch);
411
+ }
399
412
 
400
413
  try {
401
414
  await this._exec(dir, args);
402
415
  } catch (e) {
403
- if (e.message.includes("rejected")) {
404
- throw createError("PUSH_REJECTED", { message: e.message, remote, branch });
416
+ const msg = e.message || "";
417
+ const msgLower = msg.toLowerCase();
418
+
419
+ // Auto-correção: Se branch não existe no remote, tenta com -u (set-upstream)
420
+ const branchNotExists = msgLower.includes("has no upstream branch") ||
421
+ msgLower.includes("no upstream branch") ||
422
+ msgLower.includes("remote branch") && msgLower.includes("does not exist") ||
423
+ msgLower.includes("ref_not_found") ||
424
+ (msgLower.includes("fatal") && msgLower.includes("current branch") && msgLower.includes("has no upstream"));
425
+
426
+ if (branchNotExists && !setUpstream && !force) {
427
+ console.error(`[GitAdapter] Auto-fix: branch '${branch}' não existe no remote '${remote}', tentando com --set-upstream (-u)...`);
428
+ return await this.pushOne(dir, remote, branch, force, true);
429
+ }
430
+
431
+ if (msg.includes("rejected") || msgLower.includes("non-fast-forward")) {
432
+ throw createError("PUSH_REJECTED", { message: msg, remote, branch });
405
433
  }
406
434
  throw mapExternalError(e, { type: "push", remote, branch });
407
435
  }
@@ -416,13 +444,24 @@ export class GitAdapter {
416
444
  throw createError("REMOTE_NOT_FOUND", { message: "Nenhum remote github/gitea configurado" });
417
445
  }
418
446
 
419
- // Parallel push com Promise.allSettled para melhor controle
447
+ // Parallel push com Promise.allSettled para melhor controle e auto-correção
420
448
  const pushPromises = remotes.map(async (remote) => {
421
449
  try {
422
- await this.pushOne(dir, remote, branch, force);
423
- return { remote, success: true };
450
+ await this.pushOne(dir, remote, branch, force, false);
451
+ return { remote, success: true, setUpstream: false };
424
452
  } catch (error) {
425
- return { remote, success: false, error: error.message };
453
+ const errorMsg = error.message || String(error);
454
+ // Auto-correção: Se branch não existe no remote, tenta com --set-upstream
455
+ if ((errorMsg.includes("REF_NOT_FOUND") || errorMsg.includes("remote branch") || errorMsg.includes("has no upstream")) && !force) {
456
+ try {
457
+ console.error(`[GitAdapter] Auto-fix: branch '${branch}' não existe no remote '${remote}', tentando com --set-upstream...`);
458
+ await this.pushOne(dir, remote, branch, force, true);
459
+ return { remote, success: true, setUpstream: true };
460
+ } catch (retryError) {
461
+ return { remote, success: false, error: retryError.message, triedSetUpstream: true };
462
+ }
463
+ }
464
+ return { remote, success: false, error: errorMsg };
426
465
  }
427
466
  });
428
467