@andrebuzeli/git-mcp 15.8.3 → 15.8.5

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.
@@ -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
+ }