@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.
@@ -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:\\Users\\andre\\AppData\\Local\\Programs\\Git\\mingw64\\bin\\git.exe",
14
- "C:\\Users\\andre\\AppData\\Local\\Programs\\Git\\bin\\git.exe",
15
- "C:\\Users\\andre\\AppData\\Local\\Programs\\Git\\cmd\\git.exe",
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
+ }
@@ -179,38 +179,12 @@ export function detectProjectType(projectPath) {
179
179
  }
180
180
 
181
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
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
- 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;
187
+ return _withRetry(operation, { maxRetries });
215
188
  }
216
189
 
190
+
@@ -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
- }
@@ -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
- }