@andre.buzeli/git-mcp 16.0.8 → 16.1.2
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 +32 -29
- package/src/index.js +159 -147
- package/src/tools/git-branches.js +13 -3
- package/src/tools/git-clone.js +48 -85
- package/src/tools/git-config.js +13 -3
- package/src/tools/git-diff.js +121 -137
- package/src/tools/git-files.js +13 -3
- package/src/tools/git-help.js +322 -284
- package/src/tools/git-history.js +13 -3
- package/src/tools/git-ignore.js +13 -3
- package/src/tools/git-issues.js +13 -3
- package/src/tools/git-merge.js +13 -3
- package/src/tools/git-pulls.js +14 -4
- package/src/tools/git-remote.js +503 -492
- package/src/tools/git-reset.js +23 -4
- package/src/tools/git-stash.js +13 -3
- package/src/tools/git-sync.js +13 -3
- package/src/tools/git-tags.js +13 -3
- package/src/tools/git-workflow.js +599 -456
- package/src/tools/git-worktree.js +180 -0
- package/src/utils/errors.js +434 -433
- package/src/utils/gitAdapter.js +118 -6
- package/src/utils/mcpNotify.js +45 -0
- package/src/utils/repoHelpers.js +5 -31
- package/src/utils/hooks.js +0 -255
- package/src/utils/metrics.js +0 -198
package/src/utils/gitAdapter.js
CHANGED
|
@@ -7,12 +7,10 @@ import { getRepoNameFromPath, getProvidersEnv, findGitRoot } from "./repoHelpers
|
|
|
7
7
|
// Common locations for Git on Windows, Linux, and macOS
|
|
8
8
|
const GIT_CANDIDATES = [
|
|
9
9
|
// Windows paths
|
|
10
|
-
"C:\\Program Files\\Git\\mingw64\\bin\\git.exe",
|
|
11
|
-
"C:\\Program Files\\Git\\bin\\git.exe",
|
|
12
10
|
"C:\\Program Files\\Git\\cmd\\git.exe",
|
|
13
|
-
"C:\\
|
|
14
|
-
"C:\\
|
|
15
|
-
"C:\\
|
|
11
|
+
"C:\\Program Files\\Git\\bin\\git.exe",
|
|
12
|
+
"C:\\Program Files\\Git\\mingw64\\bin\\git.exe",
|
|
13
|
+
"C:\\Program Files (x86)\\Git\\cmd\\git.exe",
|
|
16
14
|
// Linux/macOS paths
|
|
17
15
|
"/usr/bin/git",
|
|
18
16
|
"/usr/local/bin/git",
|
|
@@ -963,7 +961,6 @@ export class GitAdapter {
|
|
|
963
961
|
// ============ DIFF/CLONE ============
|
|
964
962
|
async diff(dir, options = {}) { return await this._exec(dir, ["diff"]); }
|
|
965
963
|
async diffCommits(dir, from, to) { return await this._exec(dir, ["diff", from, to]); }
|
|
966
|
-
async diffStats(dir, from, to) { const out = await this._exec(dir, ["diff", "--stat", from, to]); return { message: out }; }
|
|
967
964
|
|
|
968
965
|
async clone(url, dir, options = {}) {
|
|
969
966
|
const { branch, depth, singleBranch } = options;
|
|
@@ -1032,6 +1029,121 @@ export class GitAdapter {
|
|
|
1032
1029
|
return Array.from(map.values());
|
|
1033
1030
|
}
|
|
1034
1031
|
|
|
1032
|
+
// ============ WORKTREE ============
|
|
1033
|
+
|
|
1034
|
+
async addWorktree(dir, branch, worktreePath) {
|
|
1035
|
+
// Verifica se branch já existe
|
|
1036
|
+
const branches = await this.listBranches(dir);
|
|
1037
|
+
const branchExists = branches.includes(branch);
|
|
1038
|
+
|
|
1039
|
+
const args = ["worktree", "add"];
|
|
1040
|
+
// Se branch não existe, cria com -b. Se existe, usa checkout normal (sem flag)
|
|
1041
|
+
if (!branchExists) {
|
|
1042
|
+
args.push("-b", branch);
|
|
1043
|
+
}
|
|
1044
|
+
args.push(worktreePath, branch);
|
|
1045
|
+
|
|
1046
|
+
await this._exec(dir, args);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
async listWorktrees(dir) {
|
|
1050
|
+
try {
|
|
1051
|
+
const out = await this._exec(dir, ["worktree", "list", "--porcelain"]);
|
|
1052
|
+
const worktrees = [];
|
|
1053
|
+
let current = {};
|
|
1054
|
+
|
|
1055
|
+
const lines = out.split("\n");
|
|
1056
|
+
for (const line of lines) {
|
|
1057
|
+
if (!line.trim()) {
|
|
1058
|
+
if (current.worktree) {
|
|
1059
|
+
worktrees.push(current);
|
|
1060
|
+
current = {};
|
|
1061
|
+
}
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const [key, ...rest] = line.split(" ");
|
|
1066
|
+
const value = rest.join(" ");
|
|
1067
|
+
|
|
1068
|
+
if (key === "worktree") {
|
|
1069
|
+
if (current.worktree) worktrees.push(current);
|
|
1070
|
+
current = { worktree: value };
|
|
1071
|
+
} else if (key === "branch") {
|
|
1072
|
+
current.branch = value.replace("refs/heads/", "");
|
|
1073
|
+
} else if (key === "HEAD") {
|
|
1074
|
+
current.head = value;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
if (current.worktree) worktrees.push(current);
|
|
1078
|
+
|
|
1079
|
+
return worktrees.map(w => ({
|
|
1080
|
+
path: w.worktree,
|
|
1081
|
+
branch: w.branch,
|
|
1082
|
+
head: w.head
|
|
1083
|
+
}));
|
|
1084
|
+
} catch (e) {
|
|
1085
|
+
// Fallback para versão antiga do git se --porcelain falhar
|
|
1086
|
+
const out = await this._exec(dir, ["worktree", "list"]);
|
|
1087
|
+
return out.split("\n").filter(Boolean).map(line => {
|
|
1088
|
+
const parts = line.split(/\s+/);
|
|
1089
|
+
const path = parts.slice(0, -2).join(" "); // Path pode ter espaços
|
|
1090
|
+
const head = parts[parts.length - 2];
|
|
1091
|
+
const branchRaw = parts[parts.length - 1];
|
|
1092
|
+
const branch = branchRaw.replace("[", "").replace("]", "");
|
|
1093
|
+
return { path, head, branch };
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
async removeWorktree(dir, worktreePath) {
|
|
1099
|
+
await this._exec(dir, ["worktree", "remove", worktreePath, "--force"]);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
async pushRefspec(dir, remote, localBranch, remoteBranch, force = false) {
|
|
1103
|
+
const remoteUrl = await this._exec(dir, ["remote", "get-url", remote]);
|
|
1104
|
+
const header = this._getAuthHeader(remoteUrl);
|
|
1105
|
+
const args = [];
|
|
1106
|
+
if (header) args.push("-c", `http.extraHeader=${header}`);
|
|
1107
|
+
args.push("push");
|
|
1108
|
+
if (force) args.push("--force");
|
|
1109
|
+
args.push(remote, `${localBranch}:${remoteBranch}`);
|
|
1110
|
+
await this._exec(dir, args);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
async getWorktreeConfigs(dir) {
|
|
1114
|
+
try {
|
|
1115
|
+
const configPath = path.join(dir, ".git", "config");
|
|
1116
|
+
if (!fs.existsSync(configPath)) return [];
|
|
1117
|
+
|
|
1118
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
1119
|
+
const results = [];
|
|
1120
|
+
const sectionRegex = /\[worktree-branch "([^"]+)"\]([\s\S]*?)(?=\[|$)/g;
|
|
1121
|
+
|
|
1122
|
+
let match;
|
|
1123
|
+
while ((match = sectionRegex.exec(content)) !== null) {
|
|
1124
|
+
const branchName = match[1];
|
|
1125
|
+
const body = match[2];
|
|
1126
|
+
const wtPath = body.match(/path\s*=\s*(.+)/)?.[1]?.trim();
|
|
1127
|
+
const channel = body.match(/channel\s*=\s*(.+)/)?.[1]?.trim() || "production";
|
|
1128
|
+
if (branchName && wtPath) {
|
|
1129
|
+
results.push({ branch: branchName, path: wtPath, channel });
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
return results;
|
|
1133
|
+
} catch (e) {
|
|
1134
|
+
console.error("[GitAdapter] Error reading worktree configs:", e);
|
|
1135
|
+
return [];
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
async setWorktreeConfig(dir, branchName, config) {
|
|
1140
|
+
// Normalizar path para usar forward slashes (compatível com git config em windows)
|
|
1141
|
+
const normalizedPath = config.path.replace(/\\/g, "/");
|
|
1142
|
+
|
|
1143
|
+
await this._exec(dir, ["config", `worktree-branch.${branchName}.path`, normalizedPath]);
|
|
1144
|
+
await this._exec(dir, ["config", `worktree-branch.${branchName}.channel`, config.channel || "production"]);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1035
1147
|
// ============ GIT LFS SUPPORT ============
|
|
1036
1148
|
|
|
1037
1149
|
/**
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Helpers para notificações MCP com graceful degradation
|
|
2
|
+
// Permite que o server funcione mesmo se o IDE não suportar features avançadas
|
|
3
|
+
|
|
4
|
+
// sendProgress - envia notificacao de progresso
|
|
5
|
+
export async function sendProgress(server, token, progress, total, message) {
|
|
6
|
+
if (!token) return;
|
|
7
|
+
try {
|
|
8
|
+
await server.notification({
|
|
9
|
+
method: "notifications/progress",
|
|
10
|
+
params: { progressToken: token, progress, total, message }
|
|
11
|
+
});
|
|
12
|
+
} catch (e) { /* IDE nao suporta — ignora */ }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// sendLog - envia log estruturado
|
|
16
|
+
export async function sendLog(server, level, message, data) {
|
|
17
|
+
try {
|
|
18
|
+
await server.notification({
|
|
19
|
+
method: "notifications/message",
|
|
20
|
+
params: { level, logger: "git-mcp", data: data || message }
|
|
21
|
+
});
|
|
22
|
+
} catch (e) { /* IDE nao suporta — ignora */ }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// requestConfirmation - pede confirmacao via elicitation
|
|
26
|
+
export async function requestConfirmation(server, message) {
|
|
27
|
+
try {
|
|
28
|
+
const result = await server.request(
|
|
29
|
+
{ method: "elicitation/create", params: {
|
|
30
|
+
mode: "form",
|
|
31
|
+
message,
|
|
32
|
+
requestedSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: { confirm: { type: "string", enum: ["yes", "no"], title: "Confirmar?" } },
|
|
35
|
+
required: ["confirm"]
|
|
36
|
+
}
|
|
37
|
+
}},
|
|
38
|
+
/* schema de validacao opcional */
|
|
39
|
+
);
|
|
40
|
+
if (result.action === "accept" && result.content?.confirm === "yes") return true;
|
|
41
|
+
return false;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
return true; // IDE nao suporta elicitation — prossegue sem confirmar (fail-open)
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/utils/repoHelpers.js
CHANGED
|
@@ -179,38 +179,12 @@ export function detectProjectType(projectPath) {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
/**
|
|
182
|
-
*
|
|
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
|
|
182
|
+
* Re-exporta withRetry de retry.js com adaptador para API legada (fn, maxRetries, context)
|
|
187
183
|
*/
|
|
184
|
+
import { withRetry as _withRetry } from "./retry.js";
|
|
185
|
+
|
|
188
186
|
export async function withRetry(operation, maxRetries = 3, context = "") {
|
|
189
|
-
|
|
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;
|
|
187
|
+
return _withRetry(operation, { maxRetries });
|
|
215
188
|
}
|
|
216
189
|
|
|
190
|
+
|
package/src/utils/hooks.js
DELETED
|
@@ -1,255 +0,0 @@
|
|
|
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
|
-
}
|
package/src/utils/metrics.js
DELETED
|
@@ -1,198 +0,0 @@
|
|
|
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
|
-
}
|