@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,216 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
|
|
4
|
+
export function getRepoNameFromPath(projectPath) {
|
|
5
|
+
const base = path.basename(projectPath).trim();
|
|
6
|
+
return base.replace(/\s+/g, "_").replace(/[^A-Za-z0-9_\-]/g, "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function normalizeProjectFilePath(projectPath, relative) {
|
|
10
|
+
const joined = path.resolve(projectPath, relative || ".");
|
|
11
|
+
return joined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Valida se um caminho é seguro (não contém path traversal)
|
|
16
|
+
* @param {string} projectPath - Caminho do projeto
|
|
17
|
+
* @throws {Error} Se o caminho contiver path traversal ou for inválido
|
|
18
|
+
*/
|
|
19
|
+
export function validateProjectPath(projectPath) {
|
|
20
|
+
if (!projectPath || typeof projectPath !== "string") {
|
|
21
|
+
throw new Error("projectPath é obrigatório e deve ser uma string");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Verifica se é um caminho absoluto
|
|
25
|
+
if (!path.isAbsolute(projectPath)) {
|
|
26
|
+
throw new Error("projectPath deve ser um caminho absoluto");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Normaliza o caminho
|
|
30
|
+
const normalized = path.resolve(projectPath);
|
|
31
|
+
|
|
32
|
+
// Verifica path traversal patterns
|
|
33
|
+
if (projectPath.includes("..") || projectPath.includes("./..")) {
|
|
34
|
+
throw new Error("Path traversal detectado: caminho não pode conter '..'");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Verifica se o diretório pai existe
|
|
38
|
+
const parentDir = path.dirname(normalized);
|
|
39
|
+
if (!fs.existsSync(parentDir)) {
|
|
40
|
+
throw new Error("Diretório pai não existe");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Verifica se o caminho não é muito longo (limite do Windows é 260 chars)
|
|
44
|
+
if (normalized.length > 260) {
|
|
45
|
+
throw new Error("Caminho muito longo (máximo 260 caracteres)");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Valida um caminho de arquivo relativo ao projeto
|
|
53
|
+
* @param {string} projectPath - Caminho do projeto
|
|
54
|
+
* @param {string} filePath - Caminho do arquivo relativo
|
|
55
|
+
* @returns {string} Caminho absoluto validado
|
|
56
|
+
*/
|
|
57
|
+
export function validateFilePath(projectPath, filePath) {
|
|
58
|
+
const normalizedProject = validateProjectPath(projectPath);
|
|
59
|
+
const absoluteFile = path.resolve(normalizedProject, filePath);
|
|
60
|
+
|
|
61
|
+
// Garante que o arquivo está dentro do projeto
|
|
62
|
+
if (!absoluteFile.startsWith(normalizedProject)) {
|
|
63
|
+
throw new Error(`Path traversal detectado: '${filePath}' tenta acessar fora do projeto`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return absoluteFile;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Encontra a raiz do repositório Git subindo a árvore de diretórios
|
|
71
|
+
* @param {string} startDir - Diretório inicial
|
|
72
|
+
* @returns {string|null} Caminho da raiz do repo ou null se não encontrado
|
|
73
|
+
*/
|
|
74
|
+
export function findGitRoot(startDir) {
|
|
75
|
+
let current = path.resolve(startDir);
|
|
76
|
+
const root = path.parse(current).root;
|
|
77
|
+
|
|
78
|
+
while (true) {
|
|
79
|
+
if (fs.existsSync(path.join(current, ".git"))) {
|
|
80
|
+
return current;
|
|
81
|
+
}
|
|
82
|
+
if (current === root) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
current = path.dirname(current);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getEnv(key) {
|
|
90
|
+
const v = process.env[key];
|
|
91
|
+
return v === undefined ? "" : String(v);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getProvidersEnv() {
|
|
95
|
+
return {
|
|
96
|
+
githubToken: getEnv("GITHUB_TOKEN"),
|
|
97
|
+
giteaUrl: getEnv("GITEA_URL"),
|
|
98
|
+
giteaToken: getEnv("GITEA_TOKEN"),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Padrões comuns de .gitignore para diferentes tipos de projeto
|
|
104
|
+
*/
|
|
105
|
+
export const GITIGNORE_TEMPLATES = {
|
|
106
|
+
node: [
|
|
107
|
+
"node_modules/",
|
|
108
|
+
"npm-debug.log*",
|
|
109
|
+
"yarn-debug.log*",
|
|
110
|
+
"yarn-error.log*",
|
|
111
|
+
".npm",
|
|
112
|
+
".yarn",
|
|
113
|
+
"dist/",
|
|
114
|
+
"build/",
|
|
115
|
+
".env",
|
|
116
|
+
".env.local",
|
|
117
|
+
".env.*.local",
|
|
118
|
+
"*.log",
|
|
119
|
+
".DS_Store",
|
|
120
|
+
"Thumbs.db",
|
|
121
|
+
"*.swp",
|
|
122
|
+
"*.swo",
|
|
123
|
+
".idea/",
|
|
124
|
+
".vscode/",
|
|
125
|
+
"*.sublime-*",
|
|
126
|
+
"coverage/",
|
|
127
|
+
".nyc_output/"
|
|
128
|
+
],
|
|
129
|
+
python: [
|
|
130
|
+
"__pycache__/",
|
|
131
|
+
"*.py[cod]",
|
|
132
|
+
"*$py.class",
|
|
133
|
+
"*.so",
|
|
134
|
+
".Python",
|
|
135
|
+
"env/",
|
|
136
|
+
"venv/",
|
|
137
|
+
".venv/",
|
|
138
|
+
"pip-log.txt",
|
|
139
|
+
"pip-delete-this-directory.txt",
|
|
140
|
+
".tox/",
|
|
141
|
+
".coverage",
|
|
142
|
+
".cache",
|
|
143
|
+
"*.egg-info/",
|
|
144
|
+
".installed.cfg",
|
|
145
|
+
"*.egg",
|
|
146
|
+
".env",
|
|
147
|
+
".vscode/",
|
|
148
|
+
".idea/"
|
|
149
|
+
],
|
|
150
|
+
general: [
|
|
151
|
+
".DS_Store",
|
|
152
|
+
"Thumbs.db",
|
|
153
|
+
"*.log",
|
|
154
|
+
"*.tmp",
|
|
155
|
+
"*.temp",
|
|
156
|
+
"*.swp",
|
|
157
|
+
"*.swo",
|
|
158
|
+
"*~",
|
|
159
|
+
".env",
|
|
160
|
+
".env.local",
|
|
161
|
+
".vscode/",
|
|
162
|
+
".idea/"
|
|
163
|
+
]
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Detecta o tipo de projeto baseado nos arquivos existentes
|
|
168
|
+
*/
|
|
169
|
+
export function detectProjectType(projectPath) {
|
|
170
|
+
if (fs.existsSync(path.join(projectPath, "package.json"))) {
|
|
171
|
+
return "node";
|
|
172
|
+
}
|
|
173
|
+
if (fs.existsSync(path.join(projectPath, "requirements.txt")) ||
|
|
174
|
+
fs.existsSync(path.join(projectPath, "setup.py")) ||
|
|
175
|
+
fs.existsSync(path.join(projectPath, "pyproject.toml"))) {
|
|
176
|
+
return "python";
|
|
177
|
+
}
|
|
178
|
+
return "general";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Executa uma operação com retries exponenciais
|
|
183
|
+
* @param {Function} operation - Função assíncrona a executar
|
|
184
|
+
* @param {number} maxRetries - Número máximo de tentativas
|
|
185
|
+
* @param {string} context - Contexto para logs
|
|
186
|
+
* @returns {Promise<any>} Resultado da operação
|
|
187
|
+
*/
|
|
188
|
+
export async function withRetry(operation, maxRetries = 3, context = "") {
|
|
189
|
+
let lastError;
|
|
190
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
191
|
+
try {
|
|
192
|
+
return await operation();
|
|
193
|
+
} catch (e) {
|
|
194
|
+
lastError = e;
|
|
195
|
+
const msg = e.message || String(e);
|
|
196
|
+
// Retry on network errors, lock files, or timeouts
|
|
197
|
+
const isRetryable = msg.includes("lock") ||
|
|
198
|
+
msg.includes("network") ||
|
|
199
|
+
msg.includes("resolve host") ||
|
|
200
|
+
msg.includes("timeout") ||
|
|
201
|
+
msg.includes("connection") ||
|
|
202
|
+
msg.includes("ECONNRESET") ||
|
|
203
|
+
msg.includes("ETIMEDOUT");
|
|
204
|
+
|
|
205
|
+
if (!isRetryable && i === 0) throw e; // Fail fast if not retryable and first attempt
|
|
206
|
+
|
|
207
|
+
if (i < maxRetries - 1) {
|
|
208
|
+
const delay = 2000 * Math.pow(2, i);
|
|
209
|
+
console.warn(`[${context}] Attempt ${i + 1} failed, retrying in ${delay}ms... Error: ${msg}`);
|
|
210
|
+
await new Promise(r => setTimeout(r, delay));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
throw lastError;
|
|
215
|
+
}
|
|
216
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Sistema de Retry com Backoff Exponencial e Rate Limit Handling
|
|
2
|
+
|
|
3
|
+
const DEFAULT_OPTIONS = {
|
|
4
|
+
maxRetries: 3,
|
|
5
|
+
initialDelay: 1000,
|
|
6
|
+
maxDelay: 10000,
|
|
7
|
+
backoffFactor: 2,
|
|
8
|
+
retryableErrors: [
|
|
9
|
+
// Network errors
|
|
10
|
+
"ETIMEDOUT", "ECONNRESET", "ENOTFOUND", "ECONNREFUSED", "ENETUNREACH", "EHOSTUNREACH",
|
|
11
|
+
// HTTP status codes
|
|
12
|
+
"timeout", "rate limit", "429", "503", "502", "504",
|
|
13
|
+
// SSL/TLS (pode ser temporário)
|
|
14
|
+
"EPROTO", "UNABLE_TO_GET_ISSUER_CERT",
|
|
15
|
+
// Outros
|
|
16
|
+
"socket hang up", "network error"
|
|
17
|
+
]
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function shouldRetry(error, options) {
|
|
21
|
+
const msg = (error?.message || String(error)).toLowerCase();
|
|
22
|
+
const code = error?.code?.toLowerCase() || "";
|
|
23
|
+
|
|
24
|
+
return options.retryableErrors.some(e =>
|
|
25
|
+
msg.includes(e.toLowerCase()) || code.includes(e.toLowerCase())
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sleep(ms) {
|
|
30
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extrai o tempo de espera do header Retry-After
|
|
35
|
+
* @param {Error} error - Erro com possível informação de rate limit
|
|
36
|
+
* @returns {number|null} - Tempo em ms para esperar, ou null se não encontrado
|
|
37
|
+
*/
|
|
38
|
+
function getRetryAfterMs(error) {
|
|
39
|
+
// Tenta extrair de headers (axios errors)
|
|
40
|
+
const headers = error?.response?.headers;
|
|
41
|
+
if (headers) {
|
|
42
|
+
const retryAfter = headers['retry-after'] || headers['x-ratelimit-reset'];
|
|
43
|
+
if (retryAfter) {
|
|
44
|
+
// Se for número de segundos
|
|
45
|
+
const seconds = parseInt(retryAfter, 10);
|
|
46
|
+
if (!isNaN(seconds)) {
|
|
47
|
+
// Se for timestamp unix (maior que ano 2000 em segundos)
|
|
48
|
+
if (seconds > 946684800) {
|
|
49
|
+
return Math.max(0, (seconds * 1000) - Date.now());
|
|
50
|
+
}
|
|
51
|
+
return seconds * 1000;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Tenta extrair de GitHub API rate limit
|
|
57
|
+
const rateLimitReset = error?.response?.headers?.['x-ratelimit-reset'];
|
|
58
|
+
if (rateLimitReset) {
|
|
59
|
+
const resetTime = parseInt(rateLimitReset, 10) * 1000;
|
|
60
|
+
return Math.max(0, resetTime - Date.now());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function withRetry(fn, options = {}) {
|
|
67
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
68
|
+
let lastError;
|
|
69
|
+
let delay = opts.initialDelay;
|
|
70
|
+
|
|
71
|
+
for (let attempt = 1; attempt <= opts.maxRetries; attempt++) {
|
|
72
|
+
try {
|
|
73
|
+
return await fn();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
lastError = error;
|
|
76
|
+
|
|
77
|
+
if (attempt === opts.maxRetries || !shouldRetry(error, opts)) {
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Verificar se há Retry-After header
|
|
82
|
+
const retryAfterMs = getRetryAfterMs(error);
|
|
83
|
+
const waitTime = retryAfterMs !== null
|
|
84
|
+
? Math.min(retryAfterMs, opts.maxDelay)
|
|
85
|
+
: delay;
|
|
86
|
+
|
|
87
|
+
if (process.env.DEBUG_AGENT_LOG) {
|
|
88
|
+
console.error(`[Retry] Attempt ${attempt}/${opts.maxRetries} failed, waiting ${waitTime}ms...`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await sleep(waitTime);
|
|
92
|
+
delay = Math.min(delay * opts.backoffFactor, opts.maxDelay);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
throw lastError;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Wrapper para axios com retry
|
|
100
|
+
export async function axiosWithRetry(axiosInstance, config, options = {}) {
|
|
101
|
+
return withRetry(() => axiosInstance(config), options);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Wrapper para operações git com retry
|
|
105
|
+
export async function gitWithRetry(fn, options = {}) {
|
|
106
|
+
return withRetry(fn, {
|
|
107
|
+
...options,
|
|
108
|
+
retryableErrors: [...DEFAULT_OPTIONS.retryableErrors, "ENOENT", "lock"]
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Wrapper específico para APIs com rate limit
|
|
114
|
+
*/
|
|
115
|
+
export async function apiWithRateLimitRetry(fn, options = {}) {
|
|
116
|
+
return withRetry(fn, {
|
|
117
|
+
maxRetries: 5,
|
|
118
|
+
initialDelay: 2000,
|
|
119
|
+
maxDelay: 60000,
|
|
120
|
+
...options,
|
|
121
|
+
retryableErrors: ["rate limit", "429", "403", "too many requests"]
|
|
122
|
+
});
|
|
123
|
+
}
|