@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,255 @@
1
+ // Sistema de Hooks para git-mcp
2
+ // Permite executar código customizado antes/depois de operações
3
+
4
+ /**
5
+ * Tipos de hooks disponíveis
6
+ */
7
+ export const HOOK_TYPES = {
8
+ // Workflow hooks
9
+ PRE_INIT: "pre:init",
10
+ POST_INIT: "post:init",
11
+ PRE_COMMIT: "pre:commit",
12
+ POST_COMMIT: "post:commit",
13
+ PRE_PUSH: "pre:push",
14
+ POST_PUSH: "post:push",
15
+ PRE_PULL: "pre:pull",
16
+ POST_PULL: "post:pull",
17
+
18
+ // Branch hooks
19
+ PRE_BRANCH_CREATE: "pre:branch:create",
20
+ POST_BRANCH_CREATE: "post:branch:create",
21
+ PRE_BRANCH_DELETE: "pre:branch:delete",
22
+ POST_BRANCH_DELETE: "post:branch:delete",
23
+ PRE_CHECKOUT: "pre:checkout",
24
+ POST_CHECKOUT: "post:checkout",
25
+
26
+ // Merge hooks
27
+ PRE_MERGE: "pre:merge",
28
+ POST_MERGE: "post:merge",
29
+ ON_CONFLICT: "on:conflict",
30
+
31
+ // Reset hooks
32
+ PRE_RESET: "pre:reset",
33
+ POST_RESET: "post:reset",
34
+
35
+ // Remote hooks
36
+ PRE_SYNC: "pre:sync",
37
+ POST_SYNC: "post:sync",
38
+
39
+ // Error handling
40
+ ON_ERROR: "on:error"
41
+ };
42
+
43
+ // Armazena hooks registrados
44
+ const registeredHooks = new Map();
45
+
46
+ /**
47
+ * Registra um hook
48
+ * @param {string} hookType - Tipo do hook (usar HOOK_TYPES)
49
+ * @param {Function} handler - Função async (context) => result
50
+ * @param {Object} options - { priority: number, name: string }
51
+ * @returns {string} - ID do hook para remover depois
52
+ */
53
+ export function registerHook(hookType, handler, options = {}) {
54
+ const { priority = 0, name = "anonymous" } = options;
55
+ const hookId = `${hookType}_${name}_${Date.now()}`;
56
+
57
+ if (!registeredHooks.has(hookType)) {
58
+ registeredHooks.set(hookType, []);
59
+ }
60
+
61
+ registeredHooks.get(hookType).push({
62
+ id: hookId,
63
+ handler,
64
+ priority,
65
+ name
66
+ });
67
+
68
+ // Ordena por prioridade (maior primeiro)
69
+ registeredHooks.get(hookType).sort((a, b) => b.priority - a.priority);
70
+
71
+ return hookId;
72
+ }
73
+
74
+ /**
75
+ * Remove um hook específico
76
+ * @param {string} hookId - ID retornado por registerHook
77
+ */
78
+ export function unregisterHook(hookId) {
79
+ for (const [type, hooks] of registeredHooks.entries()) {
80
+ const index = hooks.findIndex(h => h.id === hookId);
81
+ if (index !== -1) {
82
+ hooks.splice(index, 1);
83
+ return true;
84
+ }
85
+ }
86
+ return false;
87
+ }
88
+
89
+ /**
90
+ * Remove todos os hooks de um tipo
91
+ * @param {string} hookType - Tipo do hook
92
+ */
93
+ export function clearHooks(hookType) {
94
+ if (hookType) {
95
+ registeredHooks.delete(hookType);
96
+ } else {
97
+ registeredHooks.clear();
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Executa todos os hooks de um tipo
103
+ * @param {string} hookType - Tipo do hook
104
+ * @param {Object} context - Contexto passado para os handlers
105
+ * @returns {Object} - { success: boolean, results: [], errors: [] }
106
+ */
107
+ export async function runHooks(hookType, context = {}) {
108
+ const hooks = registeredHooks.get(hookType) || [];
109
+
110
+ if (hooks.length === 0) {
111
+ return { success: true, results: [], errors: [], skipped: true };
112
+ }
113
+
114
+ const results = [];
115
+ const errors = [];
116
+ let shouldContinue = true;
117
+
118
+ for (const hook of hooks) {
119
+ if (!shouldContinue) break;
120
+
121
+ try {
122
+ const result = await hook.handler({
123
+ ...context,
124
+ hookType,
125
+ hookName: hook.name
126
+ });
127
+
128
+ results.push({
129
+ hookId: hook.id,
130
+ name: hook.name,
131
+ success: true,
132
+ result
133
+ });
134
+
135
+ // Hook pode retornar { abort: true } para parar execução
136
+ if (result?.abort) {
137
+ shouldContinue = false;
138
+ }
139
+ } catch (error) {
140
+ errors.push({
141
+ hookId: hook.id,
142
+ name: hook.name,
143
+ error: error.message || String(error)
144
+ });
145
+
146
+ // Por padrão, continua mesmo com erro
147
+ // Hook pode definir { stopOnError: true } nas options
148
+ }
149
+ }
150
+
151
+ return {
152
+ success: errors.length === 0,
153
+ results,
154
+ errors,
155
+ aborted: !shouldContinue
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Wrapper para executar função com hooks pre/post
161
+ * @param {string} operationType - Tipo da operação (ex: "commit", "push")
162
+ * @param {Function} fn - Função principal async
163
+ * @param {Object} context - Contexto para os hooks
164
+ */
165
+ export async function withHooks(operationType, fn, context = {}) {
166
+ const preHookType = `pre:${operationType}`;
167
+ const postHookType = `post:${operationType}`;
168
+
169
+ // Pre-hooks
170
+ const preResult = await runHooks(preHookType, context);
171
+ if (preResult.aborted) {
172
+ return {
173
+ success: false,
174
+ abortedByHook: true,
175
+ hookResults: preResult,
176
+ message: `Operação abortada por hook ${preHookType}`
177
+ };
178
+ }
179
+
180
+ // Operação principal
181
+ let mainResult;
182
+ let mainError = null;
183
+
184
+ try {
185
+ mainResult = await fn();
186
+ } catch (error) {
187
+ mainError = error;
188
+
189
+ // Executa hook de erro
190
+ await runHooks(HOOK_TYPES.ON_ERROR, {
191
+ ...context,
192
+ error: {
193
+ message: error.message,
194
+ code: error.code,
195
+ stack: error.stack
196
+ }
197
+ });
198
+
199
+ throw error;
200
+ }
201
+
202
+ // Post-hooks
203
+ const postResult = await runHooks(postHookType, {
204
+ ...context,
205
+ result: mainResult
206
+ });
207
+
208
+ return mainResult;
209
+ }
210
+
211
+ /**
212
+ * Lista todos os hooks registrados
213
+ */
214
+ export function listHooks() {
215
+ const list = {};
216
+ for (const [type, hooks] of registeredHooks.entries()) {
217
+ list[type] = hooks.map(h => ({
218
+ id: h.id,
219
+ name: h.name,
220
+ priority: h.priority
221
+ }));
222
+ }
223
+ return list;
224
+ }
225
+
226
+ /**
227
+ * Verifica se há hooks registrados para um tipo
228
+ */
229
+ export function hasHooks(hookType) {
230
+ const hooks = registeredHooks.get(hookType);
231
+ return hooks && hooks.length > 0;
232
+ }
233
+
234
+ /**
235
+ * Exemplo de registro de hooks
236
+ */
237
+ export function exampleHookSetup() {
238
+ // Hook para logging
239
+ registerHook(HOOK_TYPES.POST_COMMIT, async (ctx) => {
240
+ console.log(`[Hook] Commit criado: ${ctx.result?.sha}`);
241
+ }, { name: "commit-logger", priority: 10 });
242
+
243
+ // Hook para validação pré-push
244
+ registerHook(HOOK_TYPES.PRE_PUSH, async (ctx) => {
245
+ // Exemplo: verificar se branch é protegida
246
+ if (ctx.branch === "main" && !ctx.force) {
247
+ console.warn("[Hook] Push para main sem force - considere criar PR");
248
+ }
249
+ }, { name: "branch-protection", priority: 100 });
250
+
251
+ // Hook para notificação de erro
252
+ registerHook(HOOK_TYPES.ON_ERROR, async (ctx) => {
253
+ console.error(`[Hook] Erro: ${ctx.error?.message}`);
254
+ }, { name: "error-notifier", priority: 0 });
255
+ }
@@ -0,0 +1,198 @@
1
+ // Sistema de Métricas/Telemetria para git-mcp
2
+ // Opt-in via variável de ambiente ENABLE_METRICS=true
3
+
4
+ const metricsEnabled = process.env.ENABLE_METRICS === "true";
5
+
6
+ // Armazena métricas em memória
7
+ const metricsStore = {
8
+ operations: [],
9
+ errors: [],
10
+ startTime: Date.now(),
11
+ summary: {
12
+ totalOperations: 0,
13
+ successfulOperations: 0,
14
+ failedOperations: 0,
15
+ totalDurationMs: 0,
16
+ operationsByType: {}
17
+ }
18
+ };
19
+
20
+ /**
21
+ * Registra início de uma operação
22
+ * @param {string} operation - Nome da operação (ex: "git-workflow:push")
23
+ * @param {Object} metadata - Metadados adicionais
24
+ * @returns {string} - ID da operação para tracking
25
+ */
26
+ export function startOperation(operation, metadata = {}) {
27
+ if (!metricsEnabled) return null;
28
+
29
+ const id = `${operation}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
30
+
31
+ metricsStore.operations.push({
32
+ id,
33
+ operation,
34
+ startTime: Date.now(),
35
+ endTime: null,
36
+ duration: null,
37
+ status: "running",
38
+ metadata,
39
+ error: null
40
+ });
41
+
42
+ return id;
43
+ }
44
+
45
+ /**
46
+ * Finaliza uma operação
47
+ * @param {string} id - ID da operação
48
+ * @param {string} status - "success" ou "error"
49
+ * @param {Error|null} error - Erro se houver
50
+ */
51
+ export function endOperation(id, status = "success", error = null) {
52
+ if (!metricsEnabled || !id) return;
53
+
54
+ const op = metricsStore.operations.find(o => o.id === id);
55
+ if (!op) return;
56
+
57
+ op.endTime = Date.now();
58
+ op.duration = op.endTime - op.startTime;
59
+ op.status = status;
60
+
61
+ if (error) {
62
+ op.error = {
63
+ message: error.message || String(error),
64
+ code: error.code || "UNKNOWN"
65
+ };
66
+ metricsStore.errors.push({
67
+ timestamp: Date.now(),
68
+ operation: op.operation,
69
+ error: op.error
70
+ });
71
+ metricsStore.summary.failedOperations++;
72
+ } else {
73
+ metricsStore.summary.successfulOperations++;
74
+ }
75
+
76
+ metricsStore.summary.totalOperations++;
77
+ metricsStore.summary.totalDurationMs += op.duration;
78
+
79
+ // Atualiza contagem por tipo
80
+ const opType = op.operation.split(":")[0];
81
+ metricsStore.summary.operationsByType[opType] =
82
+ (metricsStore.summary.operationsByType[opType] || 0) + 1;
83
+ }
84
+
85
+ /**
86
+ * Wrapper para medir tempo de execução de uma função
87
+ * @param {string} operation - Nome da operação
88
+ * @param {Function} fn - Função async para executar
89
+ * @param {Object} metadata - Metadados adicionais
90
+ */
91
+ export async function withMetrics(operation, fn, metadata = {}) {
92
+ const id = startOperation(operation, metadata);
93
+
94
+ try {
95
+ const result = await fn();
96
+ endOperation(id, "success");
97
+ return result;
98
+ } catch (error) {
99
+ endOperation(id, "error", error);
100
+ throw error;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Retorna resumo das métricas
106
+ */
107
+ export function getMetricsSummary() {
108
+ if (!metricsEnabled) {
109
+ return { enabled: false, message: "Métricas desabilitadas. Use ENABLE_METRICS=true para habilitar." };
110
+ }
111
+
112
+ const uptimeMs = Date.now() - metricsStore.startTime;
113
+ const avgDuration = metricsStore.summary.totalOperations > 0
114
+ ? metricsStore.summary.totalDurationMs / metricsStore.summary.totalOperations
115
+ : 0;
116
+
117
+ return {
118
+ enabled: true,
119
+ uptime: {
120
+ ms: uptimeMs,
121
+ formatted: formatDuration(uptimeMs)
122
+ },
123
+ operations: {
124
+ total: metricsStore.summary.totalOperations,
125
+ successful: metricsStore.summary.successfulOperations,
126
+ failed: metricsStore.summary.failedOperations,
127
+ successRate: metricsStore.summary.totalOperations > 0
128
+ ? ((metricsStore.summary.successfulOperations / metricsStore.summary.totalOperations) * 100).toFixed(2) + "%"
129
+ : "N/A"
130
+ },
131
+ performance: {
132
+ totalDurationMs: metricsStore.summary.totalDurationMs,
133
+ avgDurationMs: avgDuration.toFixed(2),
134
+ operationsPerMinute: uptimeMs > 0
135
+ ? ((metricsStore.summary.totalOperations / uptimeMs) * 60000).toFixed(2)
136
+ : 0
137
+ },
138
+ byType: metricsStore.summary.operationsByType,
139
+ recentErrors: metricsStore.errors.slice(-10)
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Retorna operações recentes
145
+ * @param {number} limit - Número máximo de operações
146
+ */
147
+ export function getRecentOperations(limit = 20) {
148
+ if (!metricsEnabled) return [];
149
+ return metricsStore.operations.slice(-limit);
150
+ }
151
+
152
+ /**
153
+ * Limpa métricas antigas
154
+ * @param {number} maxAgeMs - Idade máxima em ms (default: 1 hora)
155
+ */
156
+ export function pruneMetrics(maxAgeMs = 3600000) {
157
+ if (!metricsEnabled) return;
158
+
159
+ const cutoff = Date.now() - maxAgeMs;
160
+ metricsStore.operations = metricsStore.operations.filter(o => o.startTime > cutoff);
161
+ metricsStore.errors = metricsStore.errors.filter(e => e.timestamp > cutoff);
162
+ }
163
+
164
+ /**
165
+ * Reseta todas as métricas
166
+ */
167
+ export function resetMetrics() {
168
+ metricsStore.operations = [];
169
+ metricsStore.errors = [];
170
+ metricsStore.startTime = Date.now();
171
+ metricsStore.summary = {
172
+ totalOperations: 0,
173
+ successfulOperations: 0,
174
+ failedOperations: 0,
175
+ totalDurationMs: 0,
176
+ operationsByType: {}
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Formata duração em formato legível
182
+ */
183
+ function formatDuration(ms) {
184
+ const seconds = Math.floor(ms / 1000);
185
+ const minutes = Math.floor(seconds / 60);
186
+ const hours = Math.floor(minutes / 60);
187
+
188
+ if (hours > 0) return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
189
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
190
+ return `${seconds}s`;
191
+ }
192
+
193
+ /**
194
+ * Verifica se métricas estão habilitadas
195
+ */
196
+ export function isMetricsEnabled() {
197
+ return metricsEnabled;
198
+ }
@@ -0,0 +1,61 @@
1
+ import { withRetry } from "./retry.js";
2
+
3
+ /**
4
+ * Executa operações em GitHub e Gitea em PARALELO com retry automático
5
+ * @param {ProviderManager} pm - Provider manager
6
+ * @param {Object} handlers - { github: async (owner) => {}, gitea: async (owner) => {} }
7
+ * @param {Object} options - { retry: boolean, maxRetries: number, organization: string }
8
+ */
9
+ export async function runBoth(pm, handlers, options = {}) {
10
+ const { retry = true, maxRetries = 3, organization } = options;
11
+ const out = { github: null, gitea: null };
12
+
13
+ // Se organization fornecida, usar como owner; senão buscar owners pessoais em paralelo
14
+ const [ghOwner, geOwner] = organization
15
+ ? [organization, organization]
16
+ : await Promise.all([
17
+ pm.getGitHubOwner().catch(() => ""),
18
+ pm.getGiteaOwner().catch(() => "")
19
+ ]);
20
+
21
+ // Preparar promises para execução paralela
22
+ const promises = [];
23
+
24
+ if (pm.github && ghOwner && handlers.github) {
25
+ const githubPromise = async () => {
26
+ const executor = () => handlers.github(ghOwner);
27
+ try {
28
+ const result = retry ? await withRetry(executor, { maxRetries }) : await executor();
29
+ return { provider: "github", result };
30
+ } catch (e) {
31
+ return { provider: "github", result: { ok: false, error: String(e?.message || e) } };
32
+ }
33
+ };
34
+ promises.push(githubPromise());
35
+ }
36
+
37
+ if (pm.giteaUrl && pm.giteaToken && geOwner && handlers.gitea) {
38
+ const giteaPromise = async () => {
39
+ const executor = () => handlers.gitea(geOwner);
40
+ try {
41
+ const result = retry ? await withRetry(executor, { maxRetries }) : await executor();
42
+ return { provider: "gitea", result };
43
+ } catch (e) {
44
+ return { provider: "gitea", result: { ok: false, error: String(e?.message || e) } };
45
+ }
46
+ };
47
+ promises.push(giteaPromise());
48
+ }
49
+
50
+ // Executar em paralelo
51
+ const results = await Promise.all(promises);
52
+
53
+ // Mapear resultados
54
+ for (const { provider, result } of results) {
55
+ out[provider] = result;
56
+ }
57
+
58
+ return out;
59
+ }
60
+
61
+