@andre.buzeli/git-mcp 15.12.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.
- package/README.md +40 -0
- package/package.json +29 -0
- package/src/index.js +147 -0
- package/src/prompts/index.js +870 -0
- package/src/providers/providerManager.js +317 -0
- package/src/resources/index.js +276 -0
- package/src/tools/git-branches.js +126 -0
- package/src/tools/git-clone.js +137 -0
- package/src/tools/git-config.js +94 -0
- package/src/tools/git-diff.js +137 -0
- package/src/tools/git-files.js +82 -0
- package/src/tools/git-help.js +284 -0
- package/src/tools/git-history.js +90 -0
- package/src/tools/git-ignore.js +98 -0
- package/src/tools/git-issues.js +101 -0
- package/src/tools/git-merge.js +152 -0
- package/src/tools/git-pulls.js +115 -0
- package/src/tools/git-remote.js +492 -0
- package/src/tools/git-reset.js +105 -0
- package/src/tools/git-stash.js +120 -0
- package/src/tools/git-sync.js +129 -0
- package/src/tools/git-tags.js +113 -0
- package/src/tools/git-workflow.js +443 -0
- package/src/utils/env.js +104 -0
- package/src/utils/errors.js +431 -0
- package/src/utils/gitAdapter.js +996 -0
- package/src/utils/hooks.js +255 -0
- package/src/utils/metrics.js +198 -0
- package/src/utils/providerExec.js +61 -0
- package/src/utils/repoHelpers.js +216 -0
- package/src/utils/retry.js +123 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { MCPError, asToolError, asToolResult, errorToResponse, createError } from "../utils/errors.js";
|
|
5
|
+
import { getRepoNameFromPath, detectProjectType, GITIGNORE_TEMPLATES, validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
6
|
+
|
|
7
|
+
const ajv = new Ajv({ allErrors: true });
|
|
8
|
+
|
|
9
|
+
export function createGitWorkflowTool(pm, git) {
|
|
10
|
+
const inputSchema = {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
projectPath: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Caminho absoluto do diretório do projeto (ex: 'C:/Users/user/projeto' ou '/home/user/projeto')"
|
|
16
|
+
},
|
|
17
|
+
action: {
|
|
18
|
+
type: "string",
|
|
19
|
+
enum: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean", "update"],
|
|
20
|
+
description: `Ação a executar:
|
|
21
|
+
- init: Inicializa repositório git local E cria repos no GitHub/Gitea automaticamente
|
|
22
|
+
- status: Retorna arquivos modificados, staged, untracked (use ANTES de commit para ver o que mudou)
|
|
23
|
+
- add: Adiciona arquivos ao staging (use ANTES de commit)
|
|
24
|
+
- remove: Remove arquivos do staging
|
|
25
|
+
- commit: Cria commit com os arquivos staged (use DEPOIS de add)
|
|
26
|
+
- push: Envia commits para GitHub E Gitea em paralelo (use DEPOIS de commit)
|
|
27
|
+
- ensure-remotes: Configura remotes GitHub e Gitea (use se push falhar por falta de remote)
|
|
28
|
+
- clean: Remove arquivos não rastreados do working directory
|
|
29
|
+
- update: RECOMENDADO - Executa fluxo completo automatizado (status → add → commit → push) em uma única chamada`
|
|
30
|
+
},
|
|
31
|
+
files: {
|
|
32
|
+
type: "array",
|
|
33
|
+
items: { type: "string" },
|
|
34
|
+
description: "Lista de arquivos para add/remove. Use ['.'] para todos os arquivos. Ex: ['src/index.js', 'package.json']"
|
|
35
|
+
},
|
|
36
|
+
message: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "Mensagem do commit. Obrigatório para action='commit'. Ex: 'feat: adiciona nova funcionalidade'"
|
|
39
|
+
},
|
|
40
|
+
force: {
|
|
41
|
+
type: "boolean",
|
|
42
|
+
description: "Force push (use apenas se push normal falhar com erro de histórico divergente). Default: false"
|
|
43
|
+
},
|
|
44
|
+
createGitignore: {
|
|
45
|
+
type: "boolean",
|
|
46
|
+
description: "Se true, cria .gitignore padrão baseado no tipo de projeto (action='init'). Default: true"
|
|
47
|
+
},
|
|
48
|
+
isPublic: {
|
|
49
|
+
type: "boolean",
|
|
50
|
+
description: "Se true, repositório será PÚBLICO. Default: false (privado). Aplica-se a action='init' e 'ensure-remotes'"
|
|
51
|
+
},
|
|
52
|
+
dryRun: {
|
|
53
|
+
type: "boolean",
|
|
54
|
+
description: "Se true, simula a operação sem executar (útil para testes). Default: false"
|
|
55
|
+
},
|
|
56
|
+
skipIfClean: {
|
|
57
|
+
type: "boolean",
|
|
58
|
+
description: "Para action='update': se true, pula silenciosamente se não houver mudanças. Default: false"
|
|
59
|
+
},
|
|
60
|
+
gitignore: {
|
|
61
|
+
type: "array",
|
|
62
|
+
items: {
|
|
63
|
+
type: "string"
|
|
64
|
+
},
|
|
65
|
+
description: "Para action='update': lista de padrões para adicionar ao .gitignore antes de atualizar"
|
|
66
|
+
},
|
|
67
|
+
organization: {
|
|
68
|
+
type: "string",
|
|
69
|
+
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'"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
required: ["projectPath", "action"],
|
|
73
|
+
additionalProperties: false
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const description = `Operações Git essenciais com sincronização automática GitHub + Gitea.
|
|
77
|
+
|
|
78
|
+
⭐ RECOMENDADO - FLUXO AUTOMATIZADO:
|
|
79
|
+
• action="update" → Executa status, add, commit e push em uma única chamada
|
|
80
|
+
• Exemplo: { "projectPath": "/path", "action": "update", "message": "feat: nova feature" }
|
|
81
|
+
|
|
82
|
+
FLUXO MANUAL (se preferir controle individual):
|
|
83
|
+
1. git-workflow status → ver arquivos modificados
|
|
84
|
+
2. git-workflow add → adicionar arquivos ao staging
|
|
85
|
+
3. git-workflow commit → criar commit
|
|
86
|
+
4. git-workflow push → enviar para GitHub e Gitea
|
|
87
|
+
|
|
88
|
+
QUANDO USAR CADA ACTION:
|
|
89
|
+
- update: ⭐ RECOMENDADO - Fluxo completo automatizado (status → add → commit → push)
|
|
90
|
+
- status: Para verificar estado atual do repositório
|
|
91
|
+
- add: Quando há arquivos modificados para commitar
|
|
92
|
+
- commit: Após add, para salvar as mudanças
|
|
93
|
+
- push: Após commit, para sincronizar com remotes
|
|
94
|
+
- init: Apenas uma vez, para novos projetos (cria .gitignore automaticamente)
|
|
95
|
+
- ensure-remotes: Se push falhar por falta de configuração
|
|
96
|
+
- clean: Limpar arquivos não rastreados
|
|
97
|
+
|
|
98
|
+
EXEMPLOS DE USO:
|
|
99
|
+
• ⭐ Atualizar tudo: { "projectPath": "/path/to/project", "action": "update", "message": "feat: descrição" }
|
|
100
|
+
• Iniciar projeto: { "projectPath": "/path/to/project", "action": "init" }
|
|
101
|
+
• Ver mudanças: { "projectPath": "/path/to/project", "action": "status" }`;
|
|
102
|
+
|
|
103
|
+
async function handle(args) {
|
|
104
|
+
const validate = ajv.compile(inputSchema);
|
|
105
|
+
if (!validate(args || {})) {
|
|
106
|
+
return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
107
|
+
}
|
|
108
|
+
const { projectPath, action } = args;
|
|
109
|
+
try {
|
|
110
|
+
// #region agent log
|
|
111
|
+
if (process.env.DEBUG_AGENT_LOG) {
|
|
112
|
+
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(() => { });
|
|
113
|
+
}
|
|
114
|
+
// #endregion
|
|
115
|
+
|
|
116
|
+
validateProjectPath(projectPath);
|
|
117
|
+
// #region agent log
|
|
118
|
+
if (process.env.DEBUG_AGENT_LOG) {
|
|
119
|
+
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(() => { });
|
|
120
|
+
}
|
|
121
|
+
// #endregion
|
|
122
|
+
if (action === "init") {
|
|
123
|
+
if (args.dryRun) {
|
|
124
|
+
return asToolResult({
|
|
125
|
+
success: true,
|
|
126
|
+
dryRun: true,
|
|
127
|
+
message: "DRY RUN: Repositório seria inicializado localmente e nos providers",
|
|
128
|
+
repoName: getRepoNameFromPath(projectPath),
|
|
129
|
+
gitignoreCreated: shouldCreateGitignore
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const isRepo = await git.isRepo(projectPath);
|
|
134
|
+
if (!isRepo) {
|
|
135
|
+
await git.init(projectPath);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Criar .gitignore baseado no tipo de projeto (apenas se não existir ou se for novo repo)
|
|
139
|
+
const shouldCreateGitignore = args.createGitignore !== false;
|
|
140
|
+
let gitignoreCreated = false;
|
|
141
|
+
|
|
142
|
+
if (shouldCreateGitignore) {
|
|
143
|
+
const hasGitignore = fs.existsSync(path.join(projectPath, ".gitignore"));
|
|
144
|
+
if (!hasGitignore) {
|
|
145
|
+
const projectType = detectProjectType(projectPath);
|
|
146
|
+
const patterns = GITIGNORE_TEMPLATES[projectType] || GITIGNORE_TEMPLATES.general;
|
|
147
|
+
await git.createGitignore(projectPath, patterns);
|
|
148
|
+
gitignoreCreated = true;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
153
|
+
const isPublic = args.isPublic === true; // Default: privado
|
|
154
|
+
const organization = args.organization || undefined;
|
|
155
|
+
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic, organization });
|
|
156
|
+
|
|
157
|
+
// Configurar remotes com org se fornecida
|
|
158
|
+
const urls = await pm.getRemoteUrls(repo, organization);
|
|
159
|
+
await git.ensureRemotes(projectPath, { githubUrl: urls.github || "", giteaUrl: urls.gitea || "" });
|
|
160
|
+
|
|
161
|
+
return asToolResult({
|
|
162
|
+
success: true,
|
|
163
|
+
ensured,
|
|
164
|
+
isPrivate: !isPublic,
|
|
165
|
+
organization: organization || undefined,
|
|
166
|
+
gitignoreCreated,
|
|
167
|
+
message: "Repositório inicializado localmente e nos providers" + (organization ? ` [org: ${organization}]` : "") + (gitignoreCreated ? " (.gitignore criado)" : "") + (!isPublic ? " [PRIVADO]" : " [PÚBLICO]")
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (action === "status") {
|
|
171
|
+
const st = await git.status(projectPath);
|
|
172
|
+
// #region agent log
|
|
173
|
+
if (process.env.DEBUG_AGENT_LOG) {
|
|
174
|
+
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(() => { });
|
|
175
|
+
}
|
|
176
|
+
// #endregion
|
|
177
|
+
|
|
178
|
+
if (args.dryRun) {
|
|
179
|
+
return asToolResult({
|
|
180
|
+
success: true,
|
|
181
|
+
dryRun: true,
|
|
182
|
+
message: "DRY RUN: Status seria verificado",
|
|
183
|
+
...st
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Adicionar contexto para AI Agent decidir próximo passo
|
|
188
|
+
const hasUnstaged = st.not_added?.length > 0 || st.modified?.length > 0 || st.created?.length > 0;
|
|
189
|
+
const hasStaged = st.staged?.length > 0;
|
|
190
|
+
const hasConflicts = st.conflicted?.length > 0;
|
|
191
|
+
|
|
192
|
+
let _aiContext = {
|
|
193
|
+
needsAdd: hasUnstaged && !hasStaged,
|
|
194
|
+
readyToCommit: hasStaged && !hasConflicts,
|
|
195
|
+
hasConflicts: hasConflicts,
|
|
196
|
+
isClean: st.isClean,
|
|
197
|
+
suggestedAction: null
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (hasConflicts) {
|
|
201
|
+
_aiContext.suggestedAction = "Resolva os conflitos manualmente antes de continuar";
|
|
202
|
+
} else if (hasStaged) {
|
|
203
|
+
_aiContext.suggestedAction = "Use action='commit' com uma mensagem descritiva";
|
|
204
|
+
} else if (hasUnstaged) {
|
|
205
|
+
_aiContext.suggestedAction = "Use action='add' com files=['.'] para adicionar todas as mudanças";
|
|
206
|
+
} else {
|
|
207
|
+
_aiContext.suggestedAction = "Working tree limpa. Modifique arquivos ou use action='push' se há commits pendentes";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return asToolResult({ ...st, _aiContext }, { tool: 'workflow', action: 'status' });
|
|
211
|
+
}
|
|
212
|
+
if (action === "add") {
|
|
213
|
+
const files = Array.isArray(args.files) && args.files.length ? args.files : ["."];
|
|
214
|
+
|
|
215
|
+
if (args.dryRun) {
|
|
216
|
+
return asToolResult({
|
|
217
|
+
success: true,
|
|
218
|
+
dryRun: true,
|
|
219
|
+
message: `DRY RUN: Arquivos seriam adicionados: ${files.join(", ")}`,
|
|
220
|
+
files
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await git.add(projectPath, files);
|
|
225
|
+
return asToolResult({ success: true, files }, { tool: 'workflow', action: 'add' });
|
|
226
|
+
}
|
|
227
|
+
if (action === "remove") {
|
|
228
|
+
const files = Array.isArray(args.files) ? args.files : [];
|
|
229
|
+
await git.remove(projectPath, files);
|
|
230
|
+
return asToolResult({ success: true, files });
|
|
231
|
+
}
|
|
232
|
+
if (action === "commit") {
|
|
233
|
+
if (!args.message) {
|
|
234
|
+
return asToolError("MISSING_PARAMETER", "message é obrigatório para commit", { parameter: "message" });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (args.dryRun) {
|
|
238
|
+
return asToolResult({
|
|
239
|
+
success: true,
|
|
240
|
+
dryRun: true,
|
|
241
|
+
message: `DRY RUN: Commit seria criado com mensagem: "${args.message}"`
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const sha = await git.commit(projectPath, args.message);
|
|
246
|
+
// #region agent log
|
|
247
|
+
if (process.env.DEBUG_AGENT_LOG) {
|
|
248
|
+
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(() => { });
|
|
249
|
+
}
|
|
250
|
+
// #endregion
|
|
251
|
+
return asToolResult({ success: true, sha, message: args.message }, { tool: 'workflow', action: 'commit' });
|
|
252
|
+
}
|
|
253
|
+
if (action === "clean") {
|
|
254
|
+
if (args.dryRun) {
|
|
255
|
+
const result = await git.cleanUntracked(projectPath);
|
|
256
|
+
return asToolResult({
|
|
257
|
+
success: true,
|
|
258
|
+
dryRun: true,
|
|
259
|
+
message: `DRY RUN: ${result.cleaned.length} arquivo(s) seriam removidos`,
|
|
260
|
+
wouldClean: result.cleaned
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const result = await git.cleanUntracked(projectPath);
|
|
265
|
+
return asToolResult({
|
|
266
|
+
success: true,
|
|
267
|
+
...result,
|
|
268
|
+
message: result.cleaned.length > 0
|
|
269
|
+
? `${result.cleaned.length} arquivo(s) não rastreados removidos`
|
|
270
|
+
: "Nenhum arquivo para limpar"
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (action === "ensure-remotes") {
|
|
274
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
275
|
+
const isPublic = args.isPublic === true; // Default: privado
|
|
276
|
+
const organization = args.organization || undefined;
|
|
277
|
+
|
|
278
|
+
if (args.dryRun) {
|
|
279
|
+
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: false, isPublic, organization }); // Don't create for dry run
|
|
280
|
+
const urls = await pm.getRemoteUrls(repo, organization);
|
|
281
|
+
|
|
282
|
+
return asToolResult({
|
|
283
|
+
success: true,
|
|
284
|
+
dryRun: true,
|
|
285
|
+
message: "DRY RUN: Remotes seriam configurados" + (organization ? ` [org: ${organization}]` : ""),
|
|
286
|
+
repo,
|
|
287
|
+
organization: organization || undefined,
|
|
288
|
+
githubUrl: urls.github || "",
|
|
289
|
+
giteaUrl: urls.gitea || "",
|
|
290
|
+
ensured
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic, organization });
|
|
295
|
+
const urls = await pm.getRemoteUrls(repo, organization);
|
|
296
|
+
await git.ensureRemotes(projectPath, { githubUrl: urls.github || "", giteaUrl: urls.gitea || "" });
|
|
297
|
+
const remotes = await git.listRemotes(projectPath);
|
|
298
|
+
return asToolResult({ success: true, ensured, remotes, isPrivate: !isPublic, organization: organization || undefined }, { tool: 'workflow', action: 'ensure-remotes' });
|
|
299
|
+
}
|
|
300
|
+
if (action === "push") {
|
|
301
|
+
const branch = await git.getCurrentBranch(projectPath);
|
|
302
|
+
const force = !!args.force;
|
|
303
|
+
|
|
304
|
+
if (args.dryRun) {
|
|
305
|
+
return asToolResult({
|
|
306
|
+
success: true,
|
|
307
|
+
dryRun: true,
|
|
308
|
+
message: `DRY RUN: Push seria executado na branch '${branch}'${force ? ' (force)' : ''}`,
|
|
309
|
+
branch,
|
|
310
|
+
force
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Retry logic for push (often fails due to network or concurrent updates)
|
|
315
|
+
const result = await withRetry(
|
|
316
|
+
() => git.pushParallel(projectPath, branch, force),
|
|
317
|
+
3,
|
|
318
|
+
"push"
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
return asToolResult({ success: true, branch, ...result });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ============ UPDATE - FLUXO AUTOMATIZADO COMPLETO ============
|
|
325
|
+
if (action === "update") {
|
|
326
|
+
if (!args.message) {
|
|
327
|
+
return asToolError("MISSING_PARAMETER", "message é obrigatório para update", { parameter: "message" });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const files = Array.isArray(args.files) && args.files.length ? args.files : ["."];
|
|
331
|
+
const force = !!args.force;
|
|
332
|
+
const skipIfClean = !!args.skipIfClean;
|
|
333
|
+
const gitignorePatterns = Array.isArray(args.gitignore) ? args.gitignore : [];
|
|
334
|
+
|
|
335
|
+
// 0. Gitignore (se solicitado)
|
|
336
|
+
let gitignored = [];
|
|
337
|
+
if (gitignorePatterns.length > 0) {
|
|
338
|
+
if (args.dryRun) {
|
|
339
|
+
gitignored = gitignorePatterns;
|
|
340
|
+
} else {
|
|
341
|
+
await git.addToGitignore(projectPath, gitignorePatterns);
|
|
342
|
+
gitignored = gitignorePatterns;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// 1. Status para ver o que mudou
|
|
347
|
+
const status = await git.status(projectPath);
|
|
348
|
+
|
|
349
|
+
const hasChanges = !status.isClean ||
|
|
350
|
+
(status.not_added?.length > 0) ||
|
|
351
|
+
(status.modified?.length > 0) ||
|
|
352
|
+
(status.created?.length > 0) ||
|
|
353
|
+
(status.deleted?.length > 0) ||
|
|
354
|
+
(status.staged?.length > 0);
|
|
355
|
+
|
|
356
|
+
if (!hasChanges && skipIfClean && gitignored.length === 0) {
|
|
357
|
+
return asToolResult({
|
|
358
|
+
success: true,
|
|
359
|
+
skipped: true,
|
|
360
|
+
reason: "Working tree limpa, nada para atualizar",
|
|
361
|
+
status
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!hasChanges && gitignored.length === 0) {
|
|
366
|
+
return asToolResult({
|
|
367
|
+
success: false,
|
|
368
|
+
error: "NOTHING_TO_UPDATE",
|
|
369
|
+
message: "Nenhuma mudança para atualizar. Use skipIfClean=true para pular silenciosamente.",
|
|
370
|
+
status
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (args.dryRun) {
|
|
375
|
+
return asToolResult({
|
|
376
|
+
success: true,
|
|
377
|
+
dryRun: true,
|
|
378
|
+
message: "DRY RUN: Update seria executado",
|
|
379
|
+
wouldExecute: ["gitignore (opcional)", "status", "add", "commit", "push"],
|
|
380
|
+
files,
|
|
381
|
+
commitMessage: args.message,
|
|
382
|
+
gitignored,
|
|
383
|
+
status
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 2. Add
|
|
388
|
+
await git.add(projectPath, files);
|
|
389
|
+
|
|
390
|
+
// 3. Commit
|
|
391
|
+
const sha = await git.commit(projectPath, args.message);
|
|
392
|
+
|
|
393
|
+
// 3.5. Garantir remotes com organization (se fornecida)
|
|
394
|
+
const organization = args.organization || undefined;
|
|
395
|
+
if (organization) {
|
|
396
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
397
|
+
await pm.ensureRepos({ repoName: repo, createIfMissing: true, organization });
|
|
398
|
+
const urls = await pm.getRemoteUrls(repo, organization);
|
|
399
|
+
await git.ensureRemotes(projectPath, { githubUrl: urls.github || "", giteaUrl: urls.gitea || "" });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 4. Push
|
|
403
|
+
const branch = await git.getCurrentBranch(projectPath);
|
|
404
|
+
const pushResult = await withRetry(
|
|
405
|
+
() => git.pushParallel(projectPath, branch, force),
|
|
406
|
+
3,
|
|
407
|
+
"push"
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
return asToolResult({
|
|
411
|
+
success: true,
|
|
412
|
+
action: "update",
|
|
413
|
+
steps: ["status", "add", "commit", "push"],
|
|
414
|
+
sha,
|
|
415
|
+
message: args.message,
|
|
416
|
+
branch,
|
|
417
|
+
filesAdded: files,
|
|
418
|
+
gitignored,
|
|
419
|
+
organization: organization || undefined,
|
|
420
|
+
push: pushResult,
|
|
421
|
+
_aiContext: {
|
|
422
|
+
completed: true,
|
|
423
|
+
message: "Ciclo completo: arquivos adicionados, commit criado e push realizado" + (organization ? ` [org: ${organization}]` : "")
|
|
424
|
+
}
|
|
425
|
+
}, { tool: 'workflow', action: 'update' });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
|
|
429
|
+
availableActions: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean", "update"],
|
|
430
|
+
suggestion: "Use action='update' para fluxo completo automatizado"
|
|
431
|
+
});
|
|
432
|
+
} catch (e) {
|
|
433
|
+
return errorToResponse(e);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
name: "git-workflow",
|
|
439
|
+
description,
|
|
440
|
+
inputSchema,
|
|
441
|
+
handle
|
|
442
|
+
};
|
|
443
|
+
}
|
package/src/utils/env.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Suporte a arquivo .env
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Carrega variáveis de ambiente de um arquivo .env
|
|
7
|
+
* @param {string} envPath - Caminho para o arquivo .env (opcional)
|
|
8
|
+
*/
|
|
9
|
+
export function loadEnv(envPath = null) {
|
|
10
|
+
// Tenta encontrar .env no diretório do projeto ou no cwd
|
|
11
|
+
const possiblePaths = [
|
|
12
|
+
envPath,
|
|
13
|
+
path.join(process.cwd(), ".env"),
|
|
14
|
+
path.join(process.cwd(), ".env.local"),
|
|
15
|
+
// Para quando executado como pacote npm
|
|
16
|
+
path.resolve(import.meta.url.replace("file:///", "").replace(/\/[^/]+$/, ""), "../../.env")
|
|
17
|
+
].filter(Boolean);
|
|
18
|
+
|
|
19
|
+
for (const p of possiblePaths) {
|
|
20
|
+
try {
|
|
21
|
+
const normalizedPath = p.replace(/\\/g, "/").replace("file:///", "");
|
|
22
|
+
if (fs.existsSync(normalizedPath)) {
|
|
23
|
+
const content = fs.readFileSync(normalizedPath, "utf8");
|
|
24
|
+
parseEnvFile(content);
|
|
25
|
+
console.error(`[git-mcp] Loaded .env from: ${normalizedPath}`);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
// Ignora erros de leitura
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse do conteúdo de um arquivo .env
|
|
38
|
+
* @param {string} content - Conteúdo do arquivo
|
|
39
|
+
*/
|
|
40
|
+
function parseEnvFile(content) {
|
|
41
|
+
const lines = content.split("\n");
|
|
42
|
+
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
// Ignora linhas vazias e comentários
|
|
45
|
+
const trimmed = line.trim();
|
|
46
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
47
|
+
|
|
48
|
+
// Parse KEY=VALUE
|
|
49
|
+
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
50
|
+
if (match) {
|
|
51
|
+
const key = match[1].trim();
|
|
52
|
+
let value = match[2].trim();
|
|
53
|
+
|
|
54
|
+
// Remove aspas ao redor do valor
|
|
55
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
56
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
57
|
+
value = value.slice(1, -1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Só define se não existir (variáveis de ambiente têm precedência)
|
|
61
|
+
if (!(key in process.env)) {
|
|
62
|
+
process.env[key] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Retorna uma variável de ambiente com valor padrão opcional
|
|
70
|
+
* @param {string} key - Nome da variável
|
|
71
|
+
* @param {string} defaultValue - Valor padrão se não existir
|
|
72
|
+
*/
|
|
73
|
+
export function getEnvVar(key, defaultValue = "") {
|
|
74
|
+
return process.env[key] || defaultValue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Verifica se todas as variáveis obrigatórias estão definidas
|
|
79
|
+
* @param {string[]} requiredVars - Lista de variáveis obrigatórias
|
|
80
|
+
* @returns {{ valid: boolean, missing: string[] }}
|
|
81
|
+
*/
|
|
82
|
+
export function validateEnv(requiredVars) {
|
|
83
|
+
const missing = requiredVars.filter(v => !process.env[v]);
|
|
84
|
+
return {
|
|
85
|
+
valid: missing.length === 0,
|
|
86
|
+
missing
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Configuração padrão do git-mcp
|
|
92
|
+
*/
|
|
93
|
+
export const ENV_CONFIG = {
|
|
94
|
+
// Providers
|
|
95
|
+
GITHUB_TOKEN: "Token de autenticação do GitHub",
|
|
96
|
+
GITEA_URL: "URL do servidor Gitea",
|
|
97
|
+
GITEA_TOKEN: "Token de autenticação do Gitea",
|
|
98
|
+
|
|
99
|
+
// Performance
|
|
100
|
+
GIT_TIMEOUT_MS: "Timeout para operações Git em ms (default: 120000)",
|
|
101
|
+
|
|
102
|
+
// Debug
|
|
103
|
+
DEBUG_AGENT_LOG: "Habilita logging de debug"
|
|
104
|
+
};
|