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