@andrebuzeli/git-mcp 15.8.6 → 15.9.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 +1 -1
- package/src/index.js +8 -7
- package/src/utils/env.js +9 -9
- package/src/utils/gitAdapter.js +83 -61
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -91,17 +91,18 @@ server.setRequestHandler(
|
|
|
91
91
|
async (req) => {
|
|
92
92
|
const name = req.params?.name || "";
|
|
93
93
|
const args = req.params?.arguments || {};
|
|
94
|
-
|
|
94
|
+
// Progress reporting removed to prevent timeouts/unknown token errors on slow networks
|
|
95
|
+
// const progressToken = req.params?._meta?.progressToken;
|
|
96
|
+
|
|
95
97
|
const tool = tools.find(t => t.name === name);
|
|
96
98
|
if (!tool) return { content: [{ type: "text", text: `Tool não encontrada: ${name}` }], isError: true };
|
|
97
99
|
try {
|
|
98
|
-
if (progressToken) {
|
|
99
|
-
|
|
100
|
-
}
|
|
100
|
+
// if (progressToken) { await server.notification({ method: "notifications/progress", params: { progressToken, progress: 0 } }); }
|
|
101
|
+
|
|
101
102
|
const result = await tool.handle(args);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
|
|
104
|
+
// if (progressToken) { await server.notification({ method: "notifications/progress", params: { progressToken, progress: 100 } }); }
|
|
105
|
+
|
|
105
106
|
return result;
|
|
106
107
|
} catch (e) {
|
|
107
108
|
return asToolError(e.code || "ERROR", e.message || String(e));
|
package/src/utils/env.js
CHANGED
|
@@ -29,7 +29,7 @@ export function loadEnv(envPath = null) {
|
|
|
29
29
|
// Ignora erros de leitura
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
|
|
33
33
|
return false;
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -39,24 +39,24 @@ export function loadEnv(envPath = null) {
|
|
|
39
39
|
*/
|
|
40
40
|
function parseEnvFile(content) {
|
|
41
41
|
const lines = content.split("\n");
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
for (const line of lines) {
|
|
44
44
|
// Ignora linhas vazias e comentários
|
|
45
45
|
const trimmed = line.trim();
|
|
46
46
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
// Parse KEY=VALUE
|
|
49
49
|
const match = trimmed.match(/^([^=]+)=(.*)$/);
|
|
50
50
|
if (match) {
|
|
51
51
|
const key = match[1].trim();
|
|
52
52
|
let value = match[2].trim();
|
|
53
|
-
|
|
53
|
+
|
|
54
54
|
// Remove aspas ao redor do valor
|
|
55
55
|
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
56
|
-
|
|
56
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
57
57
|
value = value.slice(1, -1);
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
// Só define se não existir (variáveis de ambiente têm precedência)
|
|
61
61
|
if (!(key in process.env)) {
|
|
62
62
|
process.env[key] = value;
|
|
@@ -95,10 +95,10 @@ export const ENV_CONFIG = {
|
|
|
95
95
|
GITHUB_TOKEN: "Token de autenticação do GitHub",
|
|
96
96
|
GITEA_URL: "URL do servidor Gitea",
|
|
97
97
|
GITEA_TOKEN: "Token de autenticação do Gitea",
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
// Performance
|
|
100
|
-
GIT_TIMEOUT_MS: "Timeout para operações Git em ms (default:
|
|
101
|
-
|
|
100
|
+
GIT_TIMEOUT_MS: "Timeout para operações Git em ms (default: 120000)",
|
|
101
|
+
|
|
102
102
|
// Debug
|
|
103
103
|
DEBUG_AGENT_LOG: "Habilita logging de debug"
|
|
104
104
|
};
|
package/src/utils/gitAdapter.js
CHANGED
|
@@ -21,8 +21,8 @@ const GIT_CANDIDATES = [
|
|
|
21
21
|
"git"
|
|
22
22
|
];
|
|
23
23
|
|
|
24
|
-
// Timeout configurável via variável de ambiente (default:
|
|
25
|
-
const GIT_TIMEOUT = parseInt(process.env.GIT_TIMEOUT_MS || "
|
|
24
|
+
// Timeout configurável via variável de ambiente (default: 120 segundos para suportar drives de rede)
|
|
25
|
+
const GIT_TIMEOUT = parseInt(process.env.GIT_TIMEOUT_MS || "120000", 10);
|
|
26
26
|
|
|
27
27
|
export class GitAdapter {
|
|
28
28
|
constructor(providerManager) {
|
|
@@ -68,7 +68,7 @@ export class GitAdapter {
|
|
|
68
68
|
let stdout = [];
|
|
69
69
|
let stderr = [];
|
|
70
70
|
let killed = false;
|
|
71
|
-
|
|
71
|
+
|
|
72
72
|
// Timeout handler
|
|
73
73
|
const timeoutId = setTimeout(() => {
|
|
74
74
|
killed = true;
|
|
@@ -87,7 +87,7 @@ export class GitAdapter {
|
|
|
87
87
|
cp.on("close", code => {
|
|
88
88
|
clearTimeout(timeoutId);
|
|
89
89
|
if (killed) return; // Already rejected by timeout
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
const outStr = Buffer.concat(stdout).toString("utf8").trim();
|
|
92
92
|
const errStr = Buffer.concat(stderr).toString("utf8").trim();
|
|
93
93
|
|
|
@@ -109,7 +109,7 @@ export class GitAdapter {
|
|
|
109
109
|
return output;
|
|
110
110
|
} catch (e) {
|
|
111
111
|
const msg = e.message || "";
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
// Auto-fix 1: dubious ownership error (common on network shares)
|
|
114
114
|
if (msg.includes("dubious ownership") && !options._retried) {
|
|
115
115
|
console.error("[GitAdapter] Auto-fix: dubious ownership, adding safe.directory...");
|
|
@@ -120,21 +120,43 @@ export class GitAdapter {
|
|
|
120
120
|
console.error("[GitAdapter] Failed to configure safe.directory:", configError.message);
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
-
|
|
123
|
+
|
|
124
124
|
// Auto-fix 2: Lock file presente (outro processo git pode ter crashado)
|
|
125
|
-
if ((msg.includes(".git/index.lock") || msg.includes("Unable to create") && msg.includes("lock")) && !options._lockRetried) {
|
|
125
|
+
if ((msg.includes(".git/index.lock") || (msg.includes("Unable to create") && msg.includes("lock"))) && !options._lockRetried) {
|
|
126
126
|
const lockPath = path.join(dir, ".git", "index.lock");
|
|
127
|
+
let shouldDelete = false;
|
|
128
|
+
|
|
127
129
|
if (fs.existsSync(lockPath)) {
|
|
128
|
-
console.error("[GitAdapter] Auto-fix: removing stale .git/index.lock...");
|
|
129
130
|
try {
|
|
130
|
-
fs.
|
|
131
|
-
|
|
131
|
+
const stats = fs.statSync(lockPath);
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
const ageMs = now - stats.mtimeMs;
|
|
134
|
+
// Se arquivo tem mais de 5 minutos ou se o erro já persiste (implicado pelo fato de estarmos aqui) -> Deletar
|
|
135
|
+
// Para redes lentas, 5 min é seguro.
|
|
136
|
+
if (ageMs > 5 * 60 * 1000) {
|
|
137
|
+
console.error(`[GitAdapter] Lock file is old (${Math.round(ageMs / 1000)}s), deleting...`);
|
|
138
|
+
shouldDelete = true;
|
|
139
|
+
} else {
|
|
140
|
+
// Se é recente, pode ser um processo ativo. Mas como falhou o comando, assumimos que bloqueou.
|
|
141
|
+
// Em execuções agenticas sequenciais, dificilmente tem outro processo legítimo rodando ao mesmo tempo.
|
|
142
|
+
console.error(`[GitAdapter] Lock file found. Agentic force cleanup activated.`);
|
|
143
|
+
shouldDelete = true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (shouldDelete) {
|
|
147
|
+
console.error("[GitAdapter] Auto-fix: removing stale .git/index.lock...");
|
|
148
|
+
fs.unlinkSync(lockPath);
|
|
149
|
+
// Espera um pouco para o sistema de arquivos (especialmente SMB) registrar
|
|
150
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
151
|
+
return await this._exec(dir, args, { ...options, _lockRetried: true });
|
|
152
|
+
}
|
|
132
153
|
} catch (unlinkError) {
|
|
133
|
-
console.error("[GitAdapter] Failed to remove lock file:", unlinkError.message);
|
|
154
|
+
console.error("[GitAdapter] Failed to check/remove lock file:", unlinkError.message);
|
|
155
|
+
// Continua para lançar o erro original
|
|
134
156
|
}
|
|
135
157
|
}
|
|
136
158
|
}
|
|
137
|
-
|
|
159
|
+
|
|
138
160
|
// Auto-fix 3: CRLF warnings (just retry with autocrlf config)
|
|
139
161
|
if (msg.includes("CRLF will be replaced") && !options._crlfRetried) {
|
|
140
162
|
console.error("[GitAdapter] Auto-fix: configuring core.autocrlf...");
|
|
@@ -147,7 +169,7 @@ export class GitAdapter {
|
|
|
147
169
|
}
|
|
148
170
|
|
|
149
171
|
if (options.ignoreErrors) return "";
|
|
150
|
-
|
|
172
|
+
|
|
151
173
|
// Enhance error message with context
|
|
152
174
|
const cmdStr = `git ${args.join(" ")}`;
|
|
153
175
|
const enhancedError = new Error(`Command failed: ${cmdStr}\n${msg}`);
|
|
@@ -182,12 +204,12 @@ export class GitAdapter {
|
|
|
182
204
|
*/
|
|
183
205
|
async checkIntegrity(dir) {
|
|
184
206
|
await this._ensureGitRepo(dir);
|
|
185
|
-
|
|
207
|
+
|
|
186
208
|
try {
|
|
187
209
|
// Verificação rápida de objetos
|
|
188
210
|
const output = await this._exec(dir, ["fsck", "--connectivity-only"], { ignoreErrors: true });
|
|
189
211
|
const hasErrors = output.includes("error") || output.includes("missing") || output.includes("broken");
|
|
190
|
-
|
|
212
|
+
|
|
191
213
|
return {
|
|
192
214
|
ok: !hasErrors,
|
|
193
215
|
output: output || "Repository integrity OK",
|
|
@@ -211,7 +233,7 @@ export class GitAdapter {
|
|
|
211
233
|
if (!url || typeof url !== "string") {
|
|
212
234
|
return { valid: false, error: "URL vazia ou inválida" };
|
|
213
235
|
}
|
|
214
|
-
|
|
236
|
+
|
|
215
237
|
// Padrões válidos de URL git
|
|
216
238
|
const patterns = [
|
|
217
239
|
/^https?:\/\/[^\s]+\.git$/i, // https://github.com/user/repo.git
|
|
@@ -220,9 +242,9 @@ export class GitAdapter {
|
|
|
220
242
|
/^git:\/\/[^\s]+\.git$/i, // git://github.com/user/repo.git
|
|
221
243
|
/^ssh:\/\/[^\s]+\.git$/i, // ssh://git@github.com/user/repo.git
|
|
222
244
|
];
|
|
223
|
-
|
|
245
|
+
|
|
224
246
|
const isValid = patterns.some(p => p.test(url));
|
|
225
|
-
|
|
247
|
+
|
|
226
248
|
if (!isValid) {
|
|
227
249
|
return {
|
|
228
250
|
valid: false,
|
|
@@ -230,7 +252,7 @@ export class GitAdapter {
|
|
|
230
252
|
suggestion: "Use: https://github.com/user/repo.git ou git@github.com:user/repo.git"
|
|
231
253
|
};
|
|
232
254
|
}
|
|
233
|
-
|
|
255
|
+
|
|
234
256
|
return { valid: true, url };
|
|
235
257
|
}
|
|
236
258
|
|
|
@@ -291,18 +313,18 @@ export class GitAdapter {
|
|
|
291
313
|
|
|
292
314
|
const currentBranch = await this.getCurrentBranch(dir);
|
|
293
315
|
return {
|
|
294
|
-
modified,
|
|
295
|
-
created,
|
|
296
|
-
deleted,
|
|
297
|
-
renamed: [],
|
|
316
|
+
modified,
|
|
317
|
+
created,
|
|
318
|
+
deleted,
|
|
319
|
+
renamed: [],
|
|
298
320
|
notAdded,
|
|
299
321
|
// Aliases para compatibilidade
|
|
300
322
|
not_added: notAdded,
|
|
301
|
-
staged,
|
|
323
|
+
staged,
|
|
302
324
|
conflicted: [],
|
|
303
325
|
currentBranch,
|
|
304
326
|
current: currentBranch, // Alias para compatibilidade
|
|
305
|
-
isClean: lines.length === 0,
|
|
327
|
+
isClean: lines.length === 0,
|
|
306
328
|
files
|
|
307
329
|
};
|
|
308
330
|
}
|
|
@@ -413,19 +435,19 @@ export class GitAdapter {
|
|
|
413
435
|
} catch (e) {
|
|
414
436
|
const msg = e.message || "";
|
|
415
437
|
const msgLower = msg.toLowerCase();
|
|
416
|
-
|
|
438
|
+
|
|
417
439
|
// Auto-correção: Se branch não existe no remote, tenta com -u (set-upstream)
|
|
418
|
-
const branchNotExists = msgLower.includes("has no upstream branch") ||
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
440
|
+
const branchNotExists = msgLower.includes("has no upstream branch") ||
|
|
441
|
+
msgLower.includes("no upstream branch") ||
|
|
442
|
+
(msgLower.includes("remote branch") && msgLower.includes("does not exist")) ||
|
|
443
|
+
msgLower.includes("ref_not_found") ||
|
|
444
|
+
(msgLower.includes("fatal") && msgLower.includes("current branch") && msgLower.includes("has no upstream"));
|
|
445
|
+
|
|
424
446
|
if (branchNotExists && !setUpstream && !force) {
|
|
425
447
|
console.error(`[GitAdapter] Auto-fix: branch '${branch}' não existe no remote '${remote}', tentando com --set-upstream (-u)...`);
|
|
426
448
|
return await this.pushOne(dir, remote, branch, force, true);
|
|
427
449
|
}
|
|
428
|
-
|
|
450
|
+
|
|
429
451
|
if (msg.includes("rejected") || msgLower.includes("non-fast-forward")) {
|
|
430
452
|
throw createError("PUSH_REJECTED", { message: msg, remote, branch });
|
|
431
453
|
}
|
|
@@ -450,22 +472,22 @@ export class GitAdapter {
|
|
|
450
472
|
} catch (error) {
|
|
451
473
|
const errorMsg = error.message || String(error);
|
|
452
474
|
const errorMsgLower = errorMsg.toLowerCase();
|
|
453
|
-
|
|
475
|
+
|
|
454
476
|
// Auto-correção 1: Se repositório não existe, cria automaticamente
|
|
455
|
-
const repoNotFound = errorMsgLower.includes("repository not found") ||
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
477
|
+
const repoNotFound = errorMsgLower.includes("repository not found") ||
|
|
478
|
+
errorMsgLower.includes("repo not found") ||
|
|
479
|
+
(errorMsgLower.includes("fatal") && errorMsgLower.includes("repository") && errorMsgLower.includes("not found")) ||
|
|
480
|
+
(errorMsgLower.includes("not found") && (errorMsgLower.includes("remote") || errorMsgLower.includes("404")));
|
|
481
|
+
|
|
460
482
|
if (repoNotFound && !force) {
|
|
461
483
|
try {
|
|
462
484
|
console.error(`[GitAdapter] Auto-fix: repositório não existe no remote '${remote}', criando automaticamente...`);
|
|
463
485
|
const repoName = getRepoNameFromPath(dir);
|
|
464
486
|
const ensured = await this.pm.ensureRepos({ repoName, createIfMissing: true, isPublic: false });
|
|
465
|
-
|
|
487
|
+
|
|
466
488
|
// Atualiza remotes após criar repo
|
|
467
489
|
await this.ensureRemotes(dir, {});
|
|
468
|
-
|
|
490
|
+
|
|
469
491
|
// Tenta push novamente
|
|
470
492
|
await this.pushOne(dir, remote, branch, force, true); // Usa -u para criar branch também
|
|
471
493
|
return { remote, success: true, repoCreated: true, setUpstream: true };
|
|
@@ -473,13 +495,13 @@ export class GitAdapter {
|
|
|
473
495
|
return { remote, success: false, error: `Falhou ao criar repo: ${repoCreateError.message}`, triedCreateRepo: true };
|
|
474
496
|
}
|
|
475
497
|
}
|
|
476
|
-
|
|
498
|
+
|
|
477
499
|
// Auto-correção 2: Se branch não existe no remote, tenta com --set-upstream
|
|
478
|
-
const branchNotExists = errorMsgLower.includes("has no upstream branch") ||
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
500
|
+
const branchNotExists = errorMsgLower.includes("has no upstream branch") ||
|
|
501
|
+
errorMsgLower.includes("no upstream branch") ||
|
|
502
|
+
(errorMsgLower.includes("remote branch") && errorMsgLower.includes("does not exist")) ||
|
|
503
|
+
errorMsgLower.includes("ref_not_found");
|
|
504
|
+
|
|
483
505
|
if (branchNotExists && !force) {
|
|
484
506
|
try {
|
|
485
507
|
console.error(`[GitAdapter] Auto-fix: branch '${branch}' não existe no remote '${remote}', tentando com --set-upstream...`);
|
|
@@ -489,7 +511,7 @@ export class GitAdapter {
|
|
|
489
511
|
return { remote, success: false, error: retryError.message, triedSetUpstream: true };
|
|
490
512
|
}
|
|
491
513
|
}
|
|
492
|
-
|
|
514
|
+
|
|
493
515
|
return { remote, success: false, error: errorMsg };
|
|
494
516
|
}
|
|
495
517
|
});
|
|
@@ -499,17 +521,17 @@ export class GitAdapter {
|
|
|
499
521
|
const failed = results.filter(r => r.status === "rejected" || !r.value.success);
|
|
500
522
|
|
|
501
523
|
if (successful.length === 0) {
|
|
502
|
-
throw createError("PUSH_REJECTED", {
|
|
503
|
-
message: "Push falhou para todos os remotes",
|
|
504
|
-
errors: failed.map(f => f.value?.error || f.reason?.message || "Erro desconhecido")
|
|
524
|
+
throw createError("PUSH_REJECTED", {
|
|
525
|
+
message: "Push falhou para todos os remotes",
|
|
526
|
+
errors: failed.map(f => f.value?.error || f.reason?.message || "Erro desconhecido")
|
|
505
527
|
});
|
|
506
528
|
}
|
|
507
529
|
|
|
508
530
|
return {
|
|
509
531
|
pushed: successful.map(s => s.value.remote),
|
|
510
|
-
failed: failed.map(f => ({
|
|
511
|
-
remote: f.value?.remote || "unknown",
|
|
512
|
-
error: f.value?.error || f.reason?.message || "Erro desconhecido"
|
|
532
|
+
failed: failed.map(f => ({
|
|
533
|
+
remote: f.value?.remote || "unknown",
|
|
534
|
+
error: f.value?.error || f.reason?.message || "Erro desconhecido"
|
|
513
535
|
}))
|
|
514
536
|
};
|
|
515
537
|
}
|
|
@@ -709,7 +731,7 @@ export class GitAdapter {
|
|
|
709
731
|
async resetSoft(dir, ref) { await this._exec(dir, ["reset", "--soft", ref]); }
|
|
710
732
|
async resetMixed(dir, ref) { await this._exec(dir, ["reset", "--mixed", ref]); }
|
|
711
733
|
async resetHard(dir, ref) { await this._exec(dir, ["reset", "--hard", ref]); }
|
|
712
|
-
|
|
734
|
+
|
|
713
735
|
async resetHardClean(dir, ref) {
|
|
714
736
|
await this._exec(dir, ["reset", "--hard", ref]);
|
|
715
737
|
const cleanResult = await this.cleanUntracked(dir);
|
|
@@ -719,19 +741,19 @@ export class GitAdapter {
|
|
|
719
741
|
async getMergeStatus(dir) {
|
|
720
742
|
const mergeHeadPath = path.join(dir, ".git", "MERGE_HEAD");
|
|
721
743
|
const isMerging = fs.existsSync(mergeHeadPath);
|
|
722
|
-
|
|
744
|
+
|
|
723
745
|
if (!isMerging) {
|
|
724
746
|
return { merging: false, message: "Nenhum merge em andamento" };
|
|
725
747
|
}
|
|
726
|
-
|
|
748
|
+
|
|
727
749
|
// Get conflicted files
|
|
728
750
|
try {
|
|
729
751
|
const diff = await this._exec(dir, ["diff", "--name-only", "--diff-filter=U"]);
|
|
730
752
|
const conflicts = diff.split("\n").filter(Boolean);
|
|
731
|
-
return {
|
|
732
|
-
merging: true,
|
|
753
|
+
return {
|
|
754
|
+
merging: true,
|
|
733
755
|
conflicts,
|
|
734
|
-
message: conflicts.length > 0
|
|
756
|
+
message: conflicts.length > 0
|
|
735
757
|
? `Merge em andamento com ${conflicts.length} conflito(s)`
|
|
736
758
|
: "Merge em andamento sem conflitos"
|
|
737
759
|
};
|
|
@@ -841,7 +863,7 @@ export class GitAdapter {
|
|
|
841
863
|
}
|
|
842
864
|
|
|
843
865
|
// ============ GIT LFS SUPPORT ============
|
|
844
|
-
|
|
866
|
+
|
|
845
867
|
/**
|
|
846
868
|
* Verifica se Git LFS está instalado
|
|
847
869
|
*/
|