@andrebuzeli/git-mcp 13.5.0 → 13.7.0
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 +1 -1
- package/src/index.js +11 -0
- package/src/providers/providerManager.js +24 -0
- package/src/tools/git-branches.js +21 -7
- package/src/tools/git-config.js +8 -9
- package/src/tools/git-files.js +18 -8
- package/src/tools/git-history.js +22 -5
- package/src/tools/git-ignore.js +19 -8
- package/src/tools/git-issues.js +3 -3
- package/src/tools/git-pulls.js +3 -3
- package/src/tools/git-remote.js +5 -3
- package/src/tools/git-reset.js +20 -5
- package/src/tools/git-stash.js +31 -10
- package/src/tools/git-sync.js +21 -5
- package/src/tools/git-tags.js +34 -13
- package/src/tools/git-workflow.js +5 -3
- package/src/utils/errors.js +276 -1
- package/src/utils/gitAdapter.js +108 -15
- package/src/utils/retry.js +58 -9
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -19,6 +19,10 @@ import { createGitSyncTool } from "./tools/git-sync.js";
|
|
|
19
19
|
import { createGitIssuesTool } from "./tools/git-issues.js";
|
|
20
20
|
import { createGitPullsTool } from "./tools/git-pulls.js";
|
|
21
21
|
|
|
22
|
+
// #region agent log
|
|
23
|
+
const debugLog = (loc, msg, data) => { fetch('http://127.0.0.1:8242/ingest/e5799a4a-1a0d-4201-a6ce-42835e6f6fc7',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:loc,message:msg,data,timestamp:Date.now(),sessionId:'debug-session'})}).catch(()=>{}); };
|
|
24
|
+
// #endregion
|
|
25
|
+
|
|
22
26
|
const transport = new StdioServerTransport();
|
|
23
27
|
const server = new Server({ name: "git-mcpv2", version: "0.0.0" });
|
|
24
28
|
server.connect(transport);
|
|
@@ -66,6 +70,10 @@ server.setRequestHandler(
|
|
|
66
70
|
const tool = tools.find(t => t.name === name);
|
|
67
71
|
if (!tool) return { content: [{ type: "text", text: `Tool não encontrada: ${name}` }], isError: true };
|
|
68
72
|
try {
|
|
73
|
+
// #region agent log
|
|
74
|
+
const startTime = Date.now();
|
|
75
|
+
debugLog('index.js:call', `START ${name}`, { action: args.action, projectPath: args.projectPath, hypothesisId: 'A' });
|
|
76
|
+
// #endregion
|
|
69
77
|
if (progressToken) {
|
|
70
78
|
await server.notification({ method: "notifications/progress", params: { progressToken, progress: 0 } });
|
|
71
79
|
}
|
|
@@ -73,6 +81,9 @@ server.setRequestHandler(
|
|
|
73
81
|
if (progressToken) {
|
|
74
82
|
await server.notification({ method: "notifications/progress", params: { progressToken, progress: 100 } });
|
|
75
83
|
}
|
|
84
|
+
// #region agent log
|
|
85
|
+
debugLog('index.js:call', `END ${name}`, { duration: Date.now() - startTime, hypothesisId: 'A' });
|
|
86
|
+
// #endregion
|
|
76
87
|
return result;
|
|
77
88
|
} catch (e) {
|
|
78
89
|
return asToolError(e.code || "ERROR", e.message || String(e));
|
|
@@ -2,6 +2,10 @@ import { Octokit } from "@octokit/rest";
|
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
import { getProvidersEnv } from "../utils/repoHelpers.js";
|
|
4
4
|
|
|
5
|
+
// #region agent log
|
|
6
|
+
const debugLog = (loc, msg, data) => { fetch('http://127.0.0.1:8242/ingest/e5799a4a-1a0d-4201-a6ce-42835e6f6fc7',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:loc,message:msg,data,timestamp:Date.now(),sessionId:'debug-session'})}).catch(()=>{}); };
|
|
7
|
+
// #endregion
|
|
8
|
+
|
|
5
9
|
export class ProviderManager {
|
|
6
10
|
constructor() {
|
|
7
11
|
const { githubToken, giteaUrl, giteaToken } = getProvidersEnv();
|
|
@@ -18,12 +22,22 @@ export class ProviderManager {
|
|
|
18
22
|
if (!this.github) return "";
|
|
19
23
|
const now = Date.now();
|
|
20
24
|
if (this._githubOwner && now - this._ownerFetchedAt < 5 * 60 * 1000) return this._githubOwner;
|
|
25
|
+
// #region agent log
|
|
26
|
+
const t0 = Date.now();
|
|
27
|
+
debugLog('providerManager.js:getGitHubOwner', 'START API call', { hypothesisId: 'B' });
|
|
28
|
+
// #endregion
|
|
21
29
|
try {
|
|
22
30
|
const me = await this.github.rest.users.getAuthenticated();
|
|
23
31
|
this._githubOwner = me.data.login || "";
|
|
24
32
|
this._ownerFetchedAt = now;
|
|
33
|
+
// #region agent log
|
|
34
|
+
debugLog('providerManager.js:getGitHubOwner', 'END API call', { duration: Date.now() - t0, owner: this._githubOwner, hypothesisId: 'B' });
|
|
35
|
+
// #endregion
|
|
25
36
|
return this._githubOwner;
|
|
26
37
|
} catch {
|
|
38
|
+
// #region agent log
|
|
39
|
+
debugLog('providerManager.js:getGitHubOwner', 'FAILED API call', { duration: Date.now() - t0, hypothesisId: 'B' });
|
|
40
|
+
// #endregion
|
|
27
41
|
return "";
|
|
28
42
|
}
|
|
29
43
|
}
|
|
@@ -32,6 +46,10 @@ export class ProviderManager {
|
|
|
32
46
|
if (!this.giteaUrl || !this.giteaToken) return "";
|
|
33
47
|
const now = Date.now();
|
|
34
48
|
if (this._giteaOwner && now - this._ownerFetchedAt < 5 * 60 * 1000) return this._giteaOwner;
|
|
49
|
+
// #region agent log
|
|
50
|
+
const t0 = Date.now();
|
|
51
|
+
debugLog('providerManager.js:getGiteaOwner', 'START API call', { hypothesisId: 'B' });
|
|
52
|
+
// #endregion
|
|
35
53
|
try {
|
|
36
54
|
const r = await axios.get(`${this.giteaUrl}/api/v1/user`, {
|
|
37
55
|
headers: { Authorization: `token ${this.giteaToken}` },
|
|
@@ -40,8 +58,14 @@ export class ProviderManager {
|
|
|
40
58
|
const d = r.data || {};
|
|
41
59
|
this._giteaOwner = d.login || d.username || "";
|
|
42
60
|
this._ownerFetchedAt = now;
|
|
61
|
+
// #region agent log
|
|
62
|
+
debugLog('providerManager.js:getGiteaOwner', 'END API call', { duration: Date.now() - t0, owner: this._giteaOwner, hypothesisId: 'B' });
|
|
63
|
+
// #endregion
|
|
43
64
|
return this._giteaOwner;
|
|
44
65
|
} catch {
|
|
66
|
+
// #region agent log
|
|
67
|
+
debugLog('providerManager.js:getGiteaOwner', 'FAILED API call', { duration: Date.now() - t0, hypothesisId: 'B' });
|
|
68
|
+
// #endregion
|
|
45
69
|
return "";
|
|
46
70
|
}
|
|
47
71
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
|
-
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
2
|
+
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
3
|
|
|
4
4
|
const ajv = new Ajv({ allErrors: true });
|
|
5
5
|
|
|
@@ -29,32 +29,46 @@ export function createGitBranchesTool(git) {
|
|
|
29
29
|
}
|
|
30
30
|
if (action === "create") {
|
|
31
31
|
const ref = args.branch;
|
|
32
|
-
if (!ref) return asToolError("
|
|
32
|
+
if (!ref) return asToolError("MISSING_PARAMETER", "branch é obrigatório", { parameter: "branch" });
|
|
33
|
+
// Auto-correção: verificar se já existe
|
|
34
|
+
const existing = await git.listBranches(projectPath, false);
|
|
35
|
+
if (existing.includes(ref)) {
|
|
36
|
+
return asToolError("BRANCH_ALREADY_EXISTS", `Branch '${ref}' já existe`, { branch: ref, existingBranches: existing });
|
|
37
|
+
}
|
|
33
38
|
await git.createBranch(projectPath, ref);
|
|
34
39
|
return asToolResult({ success: true, branch: ref });
|
|
35
40
|
}
|
|
36
41
|
if (action === "delete") {
|
|
37
42
|
const ref = args.branch;
|
|
38
|
-
if (!ref) return asToolError("
|
|
43
|
+
if (!ref) return asToolError("MISSING_PARAMETER", "branch é obrigatório", { parameter: "branch" });
|
|
44
|
+
// Auto-correção: verificar se existe e não é a atual
|
|
45
|
+
const existing = await git.listBranches(projectPath, false);
|
|
46
|
+
if (!existing.includes(ref)) {
|
|
47
|
+
return asToolError("BRANCH_NOT_FOUND", `Branch '${ref}' não encontrada`, { branch: ref, availableBranches: existing });
|
|
48
|
+
}
|
|
49
|
+
const current = await git.getCurrentBranch(projectPath);
|
|
50
|
+
if (ref === current) {
|
|
51
|
+
return asToolError("CANNOT_DELETE_CURRENT", `Não pode deletar branch atual '${ref}'`, { branch: ref, suggestion: "Faça checkout para outra branch primeiro" });
|
|
52
|
+
}
|
|
39
53
|
await git.deleteBranch(projectPath, ref, !!args.force);
|
|
40
54
|
return asToolResult({ success: true, branch: ref });
|
|
41
55
|
}
|
|
42
56
|
if (action === "rename") {
|
|
43
57
|
const oldName = args.branch;
|
|
44
58
|
const newName = args.newBranch;
|
|
45
|
-
if (!oldName || !newName) return asToolError("
|
|
59
|
+
if (!oldName || !newName) return asToolError("MISSING_PARAMETER", "branch e newBranch são obrigatórios", { parameters: ["branch", "newBranch"] });
|
|
46
60
|
await git.renameBranch(projectPath, oldName, newName);
|
|
47
61
|
return asToolResult({ success: true, from: oldName, to: newName });
|
|
48
62
|
}
|
|
49
63
|
if (action === "checkout") {
|
|
50
64
|
const ref = args.branch;
|
|
51
|
-
if (!ref) return asToolError("
|
|
65
|
+
if (!ref) return asToolError("MISSING_PARAMETER", "branch é obrigatório", { parameter: "branch" });
|
|
52
66
|
await git.checkout(projectPath, ref);
|
|
53
67
|
return asToolResult({ success: true, branch: ref });
|
|
54
68
|
}
|
|
55
|
-
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}
|
|
69
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["list", "create", "delete", "rename", "checkout"] });
|
|
56
70
|
} catch (e) {
|
|
57
|
-
return
|
|
71
|
+
return errorToResponse(e);
|
|
58
72
|
}
|
|
59
73
|
}
|
|
60
74
|
|
package/src/tools/git-config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
|
-
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
2
|
+
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
3
|
|
|
4
4
|
const ajv = new Ajv({ allErrors: true });
|
|
5
5
|
|
|
@@ -24,30 +24,29 @@ export function createGitConfigTool(git) {
|
|
|
24
24
|
const scope = args.scope || "local";
|
|
25
25
|
try {
|
|
26
26
|
if (action === "get") {
|
|
27
|
-
if (!args.key) return asToolError("
|
|
27
|
+
if (!args.key) return asToolError("MISSING_PARAMETER", "key é obrigatório", { parameter: "key" });
|
|
28
28
|
const val = await git.getConfig(projectPath, args.key, scope);
|
|
29
|
-
return asToolResult({ key: args.key, value: val });
|
|
29
|
+
return asToolResult({ key: args.key, value: val, found: val !== undefined });
|
|
30
30
|
}
|
|
31
31
|
if (action === "set") {
|
|
32
|
-
if (!args.key) return asToolError("
|
|
32
|
+
if (!args.key) return asToolError("MISSING_PARAMETER", "key é obrigatório", { parameter: "key" });
|
|
33
33
|
await git.setConfig(projectPath, args.key, args.value ?? "", scope);
|
|
34
34
|
return asToolResult({ success: true, key: args.key, value: args.value ?? "" });
|
|
35
35
|
}
|
|
36
36
|
if (action === "unset") {
|
|
37
|
-
if (!args.key) return asToolError("
|
|
37
|
+
if (!args.key) return asToolError("MISSING_PARAMETER", "key é obrigatório", { parameter: "key" });
|
|
38
38
|
await git.unsetConfig(projectPath, args.key, scope);
|
|
39
39
|
return asToolResult({ success: true, key: args.key });
|
|
40
40
|
}
|
|
41
41
|
if (action === "list") {
|
|
42
42
|
const items = await git.listConfig(projectPath, scope);
|
|
43
|
-
return asToolResult({ scope, items });
|
|
43
|
+
return asToolResult({ scope, items, count: Object.keys(items).length });
|
|
44
44
|
}
|
|
45
|
-
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}
|
|
45
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["get", "set", "unset", "list"] });
|
|
46
46
|
} catch (e) {
|
|
47
|
-
return
|
|
47
|
+
return errorToResponse(e);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
return { name: "git-config", description: "Gerencia configurações Git (local/global/system)", inputSchema, handle };
|
|
52
52
|
}
|
|
53
|
-
|
package/src/tools/git-files.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
|
-
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
2
|
+
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
3
|
|
|
4
4
|
const ajv = new Ajv({ allErrors: true });
|
|
5
5
|
|
|
@@ -23,19 +23,29 @@ export function createGitFilesTool(git) {
|
|
|
23
23
|
try {
|
|
24
24
|
if (action === "list") {
|
|
25
25
|
const files = await git.listFiles(projectPath, args.ref || "HEAD");
|
|
26
|
-
return asToolResult({ files });
|
|
26
|
+
return asToolResult({ files, count: files.length });
|
|
27
27
|
}
|
|
28
28
|
if (action === "read") {
|
|
29
|
-
if (!args.filepath) return asToolError("
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
if (!args.filepath) return asToolError("MISSING_PARAMETER", "filepath é obrigatório", { parameter: "filepath" });
|
|
30
|
+
try {
|
|
31
|
+
const text = await git.readFile(projectPath, args.filepath, args.ref || "HEAD");
|
|
32
|
+
return { content: [{ type: "text", text }], isError: false };
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// Auto-diagnóstico: listar arquivos disponíveis se não encontrar
|
|
35
|
+
const files = await git.listFiles(projectPath, args.ref || "HEAD").catch(() => []);
|
|
36
|
+
return asToolError("FILE_NOT_FOUND", `Arquivo '${args.filepath}' não encontrado`, {
|
|
37
|
+
filepath: args.filepath,
|
|
38
|
+
ref: args.ref || "HEAD",
|
|
39
|
+
availableFiles: files.slice(0, 20),
|
|
40
|
+
totalFiles: files.length
|
|
41
|
+
});
|
|
42
|
+
}
|
|
32
43
|
}
|
|
33
|
-
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}
|
|
44
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["list", "read"] });
|
|
34
45
|
} catch (e) {
|
|
35
|
-
return
|
|
46
|
+
return errorToResponse(e);
|
|
36
47
|
}
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
return { name: "git-files", description: "Lista e lê arquivos do repo", inputSchema, handle };
|
|
40
51
|
}
|
|
41
|
-
|
package/src/tools/git-history.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
|
-
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
2
|
+
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
3
|
|
|
4
4
|
const ajv = new Ajv({ allErrors: true });
|
|
5
5
|
|
|
@@ -23,14 +23,31 @@ export function createGitHistoryTool(git) {
|
|
|
23
23
|
try {
|
|
24
24
|
if (action === "log") {
|
|
25
25
|
const items = await git.log(projectPath, { ref: args.ref || "HEAD", maxCount: args.maxCount || 50 });
|
|
26
|
-
|
|
26
|
+
if (items.length === 0) {
|
|
27
|
+
return asToolResult({
|
|
28
|
+
commits: [],
|
|
29
|
+
count: 0,
|
|
30
|
+
message: "Nenhum commit encontrado. Use action='commit' para criar o primeiro commit."
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return asToolResult({
|
|
34
|
+
commits: items.map(c => ({
|
|
35
|
+
sha: c.sha,
|
|
36
|
+
shortSha: c.sha.substring(0, 7),
|
|
37
|
+
message: c.message.split("\n")[0],
|
|
38
|
+
fullMessage: c.message,
|
|
39
|
+
author: c.author.name,
|
|
40
|
+
email: c.author.email,
|
|
41
|
+
date: c.date
|
|
42
|
+
})),
|
|
43
|
+
count: items.length
|
|
44
|
+
});
|
|
27
45
|
}
|
|
28
|
-
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}
|
|
46
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["log"] });
|
|
29
47
|
} catch (e) {
|
|
30
|
-
return
|
|
48
|
+
return errorToResponse(e);
|
|
31
49
|
}
|
|
32
50
|
}
|
|
33
51
|
|
|
34
52
|
return { name: "git-history", description: "Histórico de commits", inputSchema, handle };
|
|
35
53
|
}
|
|
36
|
-
|
package/src/tools/git-ignore.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
|
-
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
2
|
+
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
3
|
|
|
4
4
|
const ajv = new Ajv({ allErrors: true });
|
|
5
5
|
|
|
@@ -23,26 +23,37 @@ export function createGitIgnoreTool(git) {
|
|
|
23
23
|
try {
|
|
24
24
|
if (action === "list") {
|
|
25
25
|
const items = await git.listGitignore(projectPath);
|
|
26
|
-
return asToolResult({ items });
|
|
26
|
+
return asToolResult({ items, count: items.length });
|
|
27
27
|
}
|
|
28
28
|
if (action === "create") {
|
|
29
|
+
if (patterns.length === 0) {
|
|
30
|
+
return asToolError("MISSING_PARAMETER", "patterns é obrigatório para criar .gitignore", {
|
|
31
|
+
parameter: "patterns",
|
|
32
|
+
suggestion: "Forneça um array de padrões, ex: ['node_modules/', '*.log', '.env']"
|
|
33
|
+
});
|
|
34
|
+
}
|
|
29
35
|
await git.createGitignore(projectPath, patterns);
|
|
30
|
-
return asToolResult({ success: true });
|
|
36
|
+
return asToolResult({ success: true, patterns });
|
|
31
37
|
}
|
|
32
38
|
if (action === "add") {
|
|
39
|
+
if (patterns.length === 0) {
|
|
40
|
+
return asToolError("MISSING_PARAMETER", "patterns é obrigatório para adicionar ao .gitignore", { parameter: "patterns" });
|
|
41
|
+
}
|
|
33
42
|
await git.addToGitignore(projectPath, patterns);
|
|
34
|
-
return asToolResult({ success: true });
|
|
43
|
+
return asToolResult({ success: true, added: patterns });
|
|
35
44
|
}
|
|
36
45
|
if (action === "remove") {
|
|
46
|
+
if (patterns.length === 0) {
|
|
47
|
+
return asToolError("MISSING_PARAMETER", "patterns é obrigatório para remover do .gitignore", { parameter: "patterns" });
|
|
48
|
+
}
|
|
37
49
|
await git.removeFromGitignore(projectPath, patterns);
|
|
38
|
-
return asToolResult({ success: true });
|
|
50
|
+
return asToolResult({ success: true, removed: patterns });
|
|
39
51
|
}
|
|
40
|
-
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}
|
|
52
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["list", "create", "add", "remove"] });
|
|
41
53
|
} catch (e) {
|
|
42
|
-
return
|
|
54
|
+
return errorToResponse(e);
|
|
43
55
|
}
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
return { name: "git-ignore", description: "Gerencia .gitignore", inputSchema, handle };
|
|
47
59
|
}
|
|
48
|
-
|
package/src/tools/git-issues.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import axios from "axios";
|
|
3
|
-
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
3
|
+
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
4
4
|
import { getRepoNameFromPath } from "../utils/repoHelpers.js";
|
|
5
5
|
import { runBoth } from "../utils/providerExec.js";
|
|
6
6
|
|
|
@@ -48,9 +48,9 @@ export function createGitIssuesTool(pm) {
|
|
|
48
48
|
});
|
|
49
49
|
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
|
|
50
50
|
}
|
|
51
|
-
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${args.action}
|
|
51
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${args.action}`, { availableActions: ["create", "list", "comment"] });
|
|
52
52
|
} catch (e) {
|
|
53
|
-
return
|
|
53
|
+
return errorToResponse(e);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
package/src/tools/git-pulls.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import axios from "axios";
|
|
3
|
-
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
3
|
+
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
4
4
|
import { getRepoNameFromPath } from "../utils/repoHelpers.js";
|
|
5
5
|
import { runBoth } from "../utils/providerExec.js";
|
|
6
6
|
|
|
@@ -51,9 +51,9 @@ export function createGitPullsTool(pm) {
|
|
|
51
51
|
});
|
|
52
52
|
return asToolResult({ providers: out });
|
|
53
53
|
}
|
|
54
|
-
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${args.action}
|
|
54
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${args.action}`, { availableActions: ["create", "list", "files"] });
|
|
55
55
|
} catch (e) {
|
|
56
|
-
return
|
|
56
|
+
return errorToResponse(e);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
package/src/tools/git-remote.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import axios from "axios";
|
|
3
|
-
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
3
|
+
import { asToolError, asToolResult, errorToResponse, mapExternalError } from "../utils/errors.js";
|
|
4
4
|
import { getRepoNameFromPath } from "../utils/repoHelpers.js";
|
|
5
5
|
import { runBoth } from "../utils/providerExec.js";
|
|
6
6
|
|
|
@@ -167,9 +167,11 @@ export function createGitRemoteTool(pm, git) {
|
|
|
167
167
|
});
|
|
168
168
|
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), path: filePath, providers: out });
|
|
169
169
|
}
|
|
170
|
-
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}
|
|
170
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, {
|
|
171
|
+
availableActions: ["list", "ensure", "set-url", "repo-delete", "release-create", "topics-set", "milestone-create", "label-create", "fork-create", "fork-list", "star-set", "star-unset", "star-check", "subscription-set", "subscription-unset", "contents-create"]
|
|
172
|
+
});
|
|
171
173
|
} catch (e) {
|
|
172
|
-
return
|
|
174
|
+
return errorToResponse(e);
|
|
173
175
|
}
|
|
174
176
|
}
|
|
175
177
|
|
package/src/tools/git-reset.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
|
-
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
2
|
+
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
3
|
|
|
4
4
|
const ajv = new Ajv({ allErrors: true });
|
|
5
5
|
|
|
@@ -20,16 +20,31 @@ export function createGitResetTool(git) {
|
|
|
20
20
|
if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
21
21
|
const { projectPath, action, ref } = args;
|
|
22
22
|
try {
|
|
23
|
+
// Auto-verificação: checar se há commits suficientes para HEAD~N
|
|
24
|
+
if (ref.match(/HEAD~(\d+)/)) {
|
|
25
|
+
const match = ref.match(/HEAD~(\d+)/);
|
|
26
|
+
const steps = parseInt(match[1], 10);
|
|
27
|
+
const log = await git.log(projectPath, { maxCount: steps + 2 });
|
|
28
|
+
if (log.length <= steps) {
|
|
29
|
+
return asToolError("INSUFFICIENT_HISTORY", `Histórico insuficiente para ${ref}`, {
|
|
30
|
+
requestedSteps: steps,
|
|
31
|
+
availableCommits: log.length,
|
|
32
|
+
suggestion: `Use HEAD~${log.length - 1} no máximo, ou um SHA específico`,
|
|
33
|
+
recentCommits: log.slice(0, 5).map(c => ({ sha: c.sha.substring(0, 7), message: c.message.split("\n")[0] }))
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
if (action === "soft") await git.resetSoft(projectPath, ref);
|
|
24
39
|
else if (action === "mixed") await git.resetMixed(projectPath, ref);
|
|
25
40
|
else if (action === "hard") await git.resetHard(projectPath, ref);
|
|
26
|
-
else return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}
|
|
27
|
-
|
|
41
|
+
else return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["soft", "mixed", "hard"] });
|
|
42
|
+
|
|
43
|
+
return asToolResult({ success: true, action, ref, message: `Reset ${action} para ${ref} realizado` });
|
|
28
44
|
} catch (e) {
|
|
29
|
-
return
|
|
45
|
+
return errorToResponse(e);
|
|
30
46
|
}
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
return { name: "git-reset", description: "Reset soft/mixed/hard", inputSchema, handle };
|
|
34
50
|
}
|
|
35
|
-
|
package/src/tools/git-stash.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
|
-
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
2
|
+
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
3
|
|
|
4
4
|
const ajv = new Ajv({ allErrors: true });
|
|
5
5
|
|
|
@@ -24,34 +24,55 @@ export function createGitStashTool(git) {
|
|
|
24
24
|
try {
|
|
25
25
|
if (action === "list") {
|
|
26
26
|
const items = await git.listStash(projectPath);
|
|
27
|
-
return asToolResult({
|
|
27
|
+
return asToolResult({
|
|
28
|
+
items: items.map((s, i) => ({ index: i, ref: `stash@{${i}}`, message: s.message, timestamp: s.timestamp })),
|
|
29
|
+
count: items.length
|
|
30
|
+
});
|
|
28
31
|
}
|
|
29
32
|
if (action === "save") {
|
|
33
|
+
// Auto-verificação: checar se há mudanças
|
|
34
|
+
const status = await git.status(projectPath);
|
|
35
|
+
if (status.isClean && !args.includeUntracked) {
|
|
36
|
+
return asToolError("NOTHING_TO_STASH", "Working tree limpa, nada para stash", { status });
|
|
37
|
+
}
|
|
30
38
|
await git.saveStash(projectPath, args.message || "WIP", !!args.includeUntracked);
|
|
31
|
-
return asToolResult({ success: true });
|
|
39
|
+
return asToolResult({ success: true, message: args.message || "WIP" });
|
|
32
40
|
}
|
|
33
41
|
if (action === "apply") {
|
|
42
|
+
// Auto-verificação: checar se stash existe
|
|
43
|
+
const items = await git.listStash(projectPath);
|
|
44
|
+
if (items.length === 0) {
|
|
45
|
+
return asToolError("STASH_NOT_FOUND", "Nenhum stash disponível", { availableStashes: [] });
|
|
46
|
+
}
|
|
34
47
|
await git.applyStash(projectPath, args.ref);
|
|
35
|
-
return asToolResult({ success: true });
|
|
48
|
+
return asToolResult({ success: true, ref: args.ref || "stash@{0}" });
|
|
36
49
|
}
|
|
37
50
|
if (action === "pop") {
|
|
51
|
+
// Auto-verificação: checar se stash existe
|
|
52
|
+
const items = await git.listStash(projectPath);
|
|
53
|
+
if (items.length === 0) {
|
|
54
|
+
return asToolError("STASH_NOT_FOUND", "Nenhum stash disponível", { availableStashes: [] });
|
|
55
|
+
}
|
|
38
56
|
await git.popStash(projectPath, args.ref);
|
|
39
|
-
return asToolResult({ success: true });
|
|
57
|
+
return asToolResult({ success: true, ref: args.ref || "stash@{0}" });
|
|
40
58
|
}
|
|
41
59
|
if (action === "drop") {
|
|
60
|
+
const items = await git.listStash(projectPath);
|
|
61
|
+
if (items.length === 0) {
|
|
62
|
+
return asToolError("STASH_NOT_FOUND", "Nenhum stash disponível", { availableStashes: [] });
|
|
63
|
+
}
|
|
42
64
|
await git.dropStash(projectPath, args.ref);
|
|
43
|
-
return asToolResult({ success: true });
|
|
65
|
+
return asToolResult({ success: true, ref: args.ref || "stash@{0}" });
|
|
44
66
|
}
|
|
45
67
|
if (action === "clear") {
|
|
46
68
|
await git.clearStash(projectPath);
|
|
47
|
-
return asToolResult({ success: true });
|
|
69
|
+
return asToolResult({ success: true, message: "Todos os stashes removidos" });
|
|
48
70
|
}
|
|
49
|
-
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}
|
|
71
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["list", "save", "apply", "pop", "drop", "clear"] });
|
|
50
72
|
} catch (e) {
|
|
51
|
-
return
|
|
73
|
+
return errorToResponse(e);
|
|
52
74
|
}
|
|
53
75
|
}
|
|
54
76
|
|
|
55
77
|
return { name: "git-stash", description: "Gerencia stash", inputSchema, handle };
|
|
56
78
|
}
|
|
57
|
-
|
package/src/tools/git-sync.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
|
-
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
2
|
+
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
3
|
|
|
4
4
|
const ajv = new Ajv({ allErrors: true });
|
|
5
5
|
|
|
@@ -26,17 +26,33 @@ export function createGitSyncTool(git) {
|
|
|
26
26
|
const remotes = args.remote ? [args.remote] : ["origin", "github", "gitea"];
|
|
27
27
|
const promises = remotes.map(r => git.fetch(projectPath, r, branch).then(() => ({ remote: r, ok: true })).catch(e => ({ remote: r, ok: false, error: String(e?.message || e) })));
|
|
28
28
|
const results = await Promise.all(promises);
|
|
29
|
-
|
|
29
|
+
const successful = results.filter(x => x.ok);
|
|
30
|
+
const failed = results.filter(x => !x.ok);
|
|
31
|
+
return asToolResult({
|
|
32
|
+
success: successful.length > 0,
|
|
33
|
+
branch,
|
|
34
|
+
fetched: successful.map(x => x.remote),
|
|
35
|
+
failed: failed.map(x => ({ remote: x.remote, error: x.error })),
|
|
36
|
+
message: successful.length === 0 ? "Nenhum remote alcançado. Verifique conexão e configuração." : undefined
|
|
37
|
+
});
|
|
30
38
|
}
|
|
31
39
|
if (action === "pull") {
|
|
32
40
|
const remotes = args.remote ? [args.remote] : ["origin", "github", "gitea"];
|
|
33
41
|
const promises = remotes.map(r => git.pull(projectPath, r, branch).then(() => ({ remote: r, ok: true })).catch(e => ({ remote: r, ok: false, error: String(e?.message || e) })));
|
|
34
42
|
const results = await Promise.all(promises);
|
|
35
|
-
|
|
43
|
+
const successful = results.filter(x => x.ok);
|
|
44
|
+
const failed = results.filter(x => !x.ok);
|
|
45
|
+
return asToolResult({
|
|
46
|
+
success: successful.length > 0,
|
|
47
|
+
branch,
|
|
48
|
+
pulled: successful.map(x => x.remote),
|
|
49
|
+
failed: failed.map(x => ({ remote: x.remote, error: x.error })),
|
|
50
|
+
message: successful.length === 0 ? "Nenhum remote alcançado. Verifique conexão e configuração." : undefined
|
|
51
|
+
});
|
|
36
52
|
}
|
|
37
|
-
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}
|
|
53
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["pull", "fetch"] });
|
|
38
54
|
} catch (e) {
|
|
39
|
-
return
|
|
55
|
+
return errorToResponse(e);
|
|
40
56
|
}
|
|
41
57
|
}
|
|
42
58
|
|
package/src/tools/git-tags.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
|
-
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
2
|
+
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
3
|
|
|
4
4
|
const ajv = new Ajv({ allErrors: true });
|
|
5
5
|
|
|
@@ -24,35 +24,56 @@ export function createGitTagsTool(git) {
|
|
|
24
24
|
try {
|
|
25
25
|
if (action === "list") {
|
|
26
26
|
const tags = await git.listTags(projectPath);
|
|
27
|
-
return asToolResult({ tags });
|
|
27
|
+
return asToolResult({ tags, count: tags.length });
|
|
28
28
|
}
|
|
29
29
|
if (action === "create") {
|
|
30
30
|
const tag = args.tag;
|
|
31
|
-
if (!tag) return asToolError("
|
|
31
|
+
if (!tag) return asToolError("MISSING_PARAMETER", "tag é obrigatório", { parameter: "tag" });
|
|
32
|
+
// Auto-verificação: checar se já existe
|
|
33
|
+
const existing = await git.listTags(projectPath);
|
|
34
|
+
if (existing.includes(tag)) {
|
|
35
|
+
return asToolError("TAG_ALREADY_EXISTS", `Tag '${tag}' já existe`, { tag, existingTags: existing });
|
|
36
|
+
}
|
|
32
37
|
await git.createTag(projectPath, tag, args.ref || "HEAD", args.message);
|
|
33
|
-
return asToolResult({ success: true, tag });
|
|
38
|
+
return asToolResult({ success: true, tag, ref: args.ref || "HEAD" });
|
|
34
39
|
}
|
|
35
40
|
if (action === "delete") {
|
|
36
41
|
const tag = args.tag;
|
|
37
|
-
if (!tag) return asToolError("
|
|
42
|
+
if (!tag) return asToolError("MISSING_PARAMETER", "tag é obrigatório", { parameter: "tag" });
|
|
43
|
+
// Auto-verificação: checar se existe
|
|
44
|
+
const existing = await git.listTags(projectPath);
|
|
45
|
+
if (!existing.includes(tag)) {
|
|
46
|
+
return asToolError("TAG_NOT_FOUND", `Tag '${tag}' não encontrada`, { tag, existingTags: existing });
|
|
47
|
+
}
|
|
38
48
|
await git.deleteTag(projectPath, tag);
|
|
39
49
|
return asToolResult({ success: true, tag });
|
|
40
50
|
}
|
|
41
51
|
if (action === "push") {
|
|
42
52
|
const tag = args.tag;
|
|
43
|
-
if (!tag) return asToolError("
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
if (!tag) return asToolError("MISSING_PARAMETER", "tag é obrigatório", { parameter: "tag" });
|
|
54
|
+
// Auto-verificação: checar se existe
|
|
55
|
+
const existing = await git.listTags(projectPath);
|
|
56
|
+
if (!existing.includes(tag)) {
|
|
57
|
+
return asToolError("TAG_NOT_FOUND", `Tag '${tag}' não existe localmente. Crie primeiro com action='create'`, { tag, existingTags: existing });
|
|
58
|
+
}
|
|
59
|
+
const results = await Promise.allSettled([
|
|
60
|
+
git.pushTag(projectPath, "github", tag),
|
|
61
|
+
git.pushTag(projectPath, "gitea", tag)
|
|
47
62
|
]);
|
|
48
|
-
|
|
63
|
+
const pushed = [];
|
|
64
|
+
const failed = [];
|
|
65
|
+
if (results[0].status === "fulfilled") pushed.push("github");
|
|
66
|
+
else failed.push({ remote: "github", error: results[0].reason?.message });
|
|
67
|
+
if (results[1].status === "fulfilled") pushed.push("gitea");
|
|
68
|
+
else failed.push({ remote: "gitea", error: results[1].reason?.message });
|
|
69
|
+
|
|
70
|
+
return asToolResult({ success: pushed.length > 0, tag, pushed, failed });
|
|
49
71
|
}
|
|
50
|
-
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}
|
|
72
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, { availableActions: ["list", "create", "delete", "push"] });
|
|
51
73
|
} catch (e) {
|
|
52
|
-
return
|
|
74
|
+
return errorToResponse(e);
|
|
53
75
|
}
|
|
54
76
|
}
|
|
55
77
|
|
|
56
78
|
return { name: "git-tags", description: "Gerencia tags e push paralelo", inputSchema, handle };
|
|
57
79
|
}
|
|
58
|
-
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
|
-
import { MCPError, asToolError, asToolResult } from "../utils/errors.js";
|
|
2
|
+
import { MCPError, asToolError, asToolResult, errorToResponse, createError } from "../utils/errors.js";
|
|
3
3
|
import { getRepoNameFromPath } from "../utils/repoHelpers.js";
|
|
4
4
|
|
|
5
5
|
const ajv = new Ajv({ allErrors: true });
|
|
@@ -71,9 +71,11 @@ export function createGitWorkflowTool(pm, git) {
|
|
|
71
71
|
// Basic implementation: fetch + status; full merge/pull can be added
|
|
72
72
|
return asToolResult({ success: true, message: "Operação de pull/sync não-op realizada" });
|
|
73
73
|
}
|
|
74
|
-
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}
|
|
74
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`, {
|
|
75
|
+
availableActions: ["init", "status", "add", "remove", "commit", "push", "pull", "sync", "ensure-remotes"]
|
|
76
|
+
});
|
|
75
77
|
} catch (e) {
|
|
76
|
-
return
|
|
78
|
+
return errorToResponse(e);
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
|
package/src/utils/errors.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// Sistema de Erros Melhorado para AI Agents
|
|
2
|
+
|
|
1
3
|
export class MCPError extends Error {
|
|
2
4
|
constructor(code, message, data) {
|
|
3
5
|
super(message);
|
|
@@ -6,6 +8,262 @@ export class MCPError extends Error {
|
|
|
6
8
|
}
|
|
7
9
|
}
|
|
8
10
|
|
|
11
|
+
// Códigos de erro padronizados com sugestões para AI
|
|
12
|
+
export const ERROR_CODES = {
|
|
13
|
+
// Auth
|
|
14
|
+
AUTH_GITHUB_INVALID: {
|
|
15
|
+
code: "AUTH_GITHUB_INVALID",
|
|
16
|
+
suggestion: "Verifique se GITHUB_TOKEN está configurado e é válido. Gere um novo token em github.com/settings/tokens"
|
|
17
|
+
},
|
|
18
|
+
AUTH_GITEA_INVALID: {
|
|
19
|
+
code: "AUTH_GITEA_INVALID",
|
|
20
|
+
suggestion: "Verifique se GITEA_TOKEN e GITEA_URL estão configurados corretamente"
|
|
21
|
+
},
|
|
22
|
+
AUTH_NO_PERMISSION: {
|
|
23
|
+
code: "AUTH_NO_PERMISSION",
|
|
24
|
+
suggestion: "Token não tem permissão para esta operação. Verifique os scopes do token"
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// Repo
|
|
28
|
+
REPO_NOT_FOUND: {
|
|
29
|
+
code: "REPO_NOT_FOUND",
|
|
30
|
+
suggestion: "Repositório não existe. Use action='init' com createIfMissing=true para criar"
|
|
31
|
+
},
|
|
32
|
+
REPO_NO_WRITE: {
|
|
33
|
+
code: "REPO_NO_WRITE",
|
|
34
|
+
suggestion: "Sem permissão de escrita. Verifique se você é owner ou colaborador do repositório"
|
|
35
|
+
},
|
|
36
|
+
REPO_ALREADY_EXISTS: {
|
|
37
|
+
code: "REPO_ALREADY_EXISTS",
|
|
38
|
+
suggestion: "Repositório já existe. Use action='ensure' para verificar ou escolha outro nome"
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Git Local
|
|
42
|
+
NOT_A_GIT_REPO: {
|
|
43
|
+
code: "NOT_A_GIT_REPO",
|
|
44
|
+
suggestion: "Diretório não é um repositório git. Use action='init' primeiro para inicializar"
|
|
45
|
+
},
|
|
46
|
+
NO_COMMITS: {
|
|
47
|
+
code: "NO_COMMITS",
|
|
48
|
+
suggestion: "Repositório não tem commits. Use action='add' e depois action='commit' primeiro"
|
|
49
|
+
},
|
|
50
|
+
NOTHING_TO_COMMIT: {
|
|
51
|
+
code: "NOTHING_TO_COMMIT",
|
|
52
|
+
suggestion: "Working tree limpa, nada para commitar. Modifique arquivos primeiro"
|
|
53
|
+
},
|
|
54
|
+
NOTHING_TO_PUSH: {
|
|
55
|
+
code: "NOTHING_TO_PUSH",
|
|
56
|
+
suggestion: "Nenhuma mudança para push. Faça commits primeiro"
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
// Branches
|
|
60
|
+
BRANCH_NOT_FOUND: {
|
|
61
|
+
code: "BRANCH_NOT_FOUND",
|
|
62
|
+
suggestion: "Branch não existe. Use action='list' para ver branches disponíveis"
|
|
63
|
+
},
|
|
64
|
+
BRANCH_ALREADY_EXISTS: {
|
|
65
|
+
code: "BRANCH_ALREADY_EXISTS",
|
|
66
|
+
suggestion: "Branch já existe. Use outro nome ou delete a existente primeiro"
|
|
67
|
+
},
|
|
68
|
+
CANNOT_DELETE_CURRENT: {
|
|
69
|
+
code: "CANNOT_DELETE_CURRENT",
|
|
70
|
+
suggestion: "Não pode deletar branch atual. Faça checkout para outra branch primeiro"
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// Tags
|
|
74
|
+
TAG_NOT_FOUND: {
|
|
75
|
+
code: "TAG_NOT_FOUND",
|
|
76
|
+
suggestion: "Tag não existe. Use action='list' para ver tags disponíveis"
|
|
77
|
+
},
|
|
78
|
+
TAG_ALREADY_EXISTS: {
|
|
79
|
+
code: "TAG_ALREADY_EXISTS",
|
|
80
|
+
suggestion: "Tag já existe. Use outro nome ou delete a existente primeiro"
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Refs
|
|
84
|
+
REF_NOT_FOUND: {
|
|
85
|
+
code: "REF_NOT_FOUND",
|
|
86
|
+
suggestion: "Referência não encontrada. Verifique se o commit/branch/tag existe. Use action='log' para ver histórico"
|
|
87
|
+
},
|
|
88
|
+
INSUFFICIENT_HISTORY: {
|
|
89
|
+
code: "INSUFFICIENT_HISTORY",
|
|
90
|
+
suggestion: "Histórico insuficiente para HEAD~N. Use action='log' para verificar quantos commits existem"
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Stash
|
|
94
|
+
NOTHING_TO_STASH: {
|
|
95
|
+
code: "NOTHING_TO_STASH",
|
|
96
|
+
suggestion: "Working tree limpa, nada para stash. Modifique arquivos primeiro"
|
|
97
|
+
},
|
|
98
|
+
STASH_NOT_FOUND: {
|
|
99
|
+
code: "STASH_NOT_FOUND",
|
|
100
|
+
suggestion: "Stash não encontrado. Use action='list' para ver stashes disponíveis"
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// Remote
|
|
104
|
+
REMOTE_NOT_FOUND: {
|
|
105
|
+
code: "REMOTE_NOT_FOUND",
|
|
106
|
+
suggestion: "Remote não configurado. Use action='ensure-remotes' para configurar"
|
|
107
|
+
},
|
|
108
|
+
PUSH_REJECTED: {
|
|
109
|
+
code: "PUSH_REJECTED",
|
|
110
|
+
suggestion: "Push rejeitado (histórico divergente). Use force=true para forçar, ou faça pull primeiro"
|
|
111
|
+
},
|
|
112
|
+
MERGE_CONFLICT: {
|
|
113
|
+
code: "MERGE_CONFLICT",
|
|
114
|
+
suggestion: "Conflito de merge. Resolva conflitos manualmente e faça novo commit"
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// Network
|
|
118
|
+
NETWORK_TIMEOUT: {
|
|
119
|
+
code: "NETWORK_TIMEOUT",
|
|
120
|
+
suggestion: "Timeout de conexão. Verifique sua internet e tente novamente"
|
|
121
|
+
},
|
|
122
|
+
RATE_LIMIT: {
|
|
123
|
+
code: "RATE_LIMIT",
|
|
124
|
+
suggestion: "Rate limit excedido. Aguarde alguns minutos antes de tentar novamente"
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// Validation
|
|
128
|
+
VALIDATION_ERROR: {
|
|
129
|
+
code: "VALIDATION_ERROR",
|
|
130
|
+
suggestion: "Parâmetros inválidos. Verifique os parâmetros obrigatórios da action"
|
|
131
|
+
},
|
|
132
|
+
MISSING_PARAMETER: {
|
|
133
|
+
code: "MISSING_PARAMETER",
|
|
134
|
+
suggestion: "Parâmetro obrigatório faltando"
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// Files
|
|
138
|
+
FILE_NOT_FOUND: {
|
|
139
|
+
code: "FILE_NOT_FOUND",
|
|
140
|
+
suggestion: "Arquivo não encontrado no repositório. Use action='list' para ver arquivos disponíveis"
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
// Issues/PRs
|
|
144
|
+
ISSUE_NOT_FOUND: {
|
|
145
|
+
code: "ISSUE_NOT_FOUND",
|
|
146
|
+
suggestion: "Issue não encontrada. Use action='list' para ver issues disponíveis"
|
|
147
|
+
},
|
|
148
|
+
PR_NOT_FOUND: {
|
|
149
|
+
code: "PR_NOT_FOUND",
|
|
150
|
+
suggestion: "Pull Request não encontrado. Use action='list' para ver PRs disponíveis"
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// Generic
|
|
154
|
+
UNKNOWN_ERROR: {
|
|
155
|
+
code: "UNKNOWN_ERROR",
|
|
156
|
+
suggestion: "Erro desconhecido. Verifique os logs para mais detalhes"
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Função para criar erro com sugestão
|
|
161
|
+
export function createError(errorCode, details = {}) {
|
|
162
|
+
const errorInfo = ERROR_CODES[errorCode] || ERROR_CODES.UNKNOWN_ERROR;
|
|
163
|
+
return new MCPError(errorInfo.code, details.message || errorInfo.code, {
|
|
164
|
+
suggestion: errorInfo.suggestion,
|
|
165
|
+
...details
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Função para mapear erros externos para códigos internos
|
|
170
|
+
export function mapExternalError(error, context = {}) {
|
|
171
|
+
const msg = error?.message?.toLowerCase() || String(error).toLowerCase();
|
|
172
|
+
|
|
173
|
+
// GitHub/Gitea API errors
|
|
174
|
+
if (msg.includes("bad credentials") || msg.includes("401")) {
|
|
175
|
+
return context.provider === "gitea"
|
|
176
|
+
? createError("AUTH_GITEA_INVALID", { originalError: msg })
|
|
177
|
+
: createError("AUTH_GITHUB_INVALID", { originalError: msg });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (msg.includes("not found") || msg.includes("404")) {
|
|
181
|
+
if (context.type === "repo") return createError("REPO_NOT_FOUND", { repo: context.repo, originalError: msg });
|
|
182
|
+
if (context.type === "branch") return createError("BRANCH_NOT_FOUND", { branch: context.branch, originalError: msg });
|
|
183
|
+
if (context.type === "tag") return createError("TAG_NOT_FOUND", { tag: context.tag, originalError: msg });
|
|
184
|
+
if (context.type === "file") return createError("FILE_NOT_FOUND", { file: context.file, originalError: msg });
|
|
185
|
+
if (context.type === "issue") return createError("ISSUE_NOT_FOUND", { number: context.number, originalError: msg });
|
|
186
|
+
if (context.type === "pr") return createError("PR_NOT_FOUND", { number: context.number, originalError: msg });
|
|
187
|
+
return createError("REF_NOT_FOUND", { ref: context.ref, originalError: msg });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (msg.includes("permission") || msg.includes("403") || msg.includes("forbidden")) {
|
|
191
|
+
return createError("AUTH_NO_PERMISSION", { originalError: msg });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (msg.includes("rate limit") || msg.includes("429")) {
|
|
195
|
+
return createError("RATE_LIMIT", { originalError: msg });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (msg.includes("timeout") || msg.includes("etimedout") || msg.includes("econnrefused")) {
|
|
199
|
+
return createError("NETWORK_TIMEOUT", { originalError: msg });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (msg.includes("push rejected") || msg.includes("non-fast-forward")) {
|
|
203
|
+
return createError("PUSH_REJECTED", { originalError: msg });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (msg.includes("conflict")) {
|
|
207
|
+
return createError("MERGE_CONFLICT", { originalError: msg });
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (msg.includes("already exists")) {
|
|
211
|
+
if (context.type === "repo") return createError("REPO_ALREADY_EXISTS", { repo: context.repo, originalError: msg });
|
|
212
|
+
if (context.type === "branch") return createError("BRANCH_ALREADY_EXISTS", { branch: context.branch, originalError: msg });
|
|
213
|
+
if (context.type === "tag") return createError("TAG_ALREADY_EXISTS", { tag: context.tag, originalError: msg });
|
|
214
|
+
return createError("BRANCH_ALREADY_EXISTS", { originalError: msg });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// isomorphic-git specific errors
|
|
218
|
+
if (msg.includes("could not find") && msg.includes("head~")) {
|
|
219
|
+
return createError("INSUFFICIENT_HISTORY", { originalError: msg });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (msg.includes("is not a git repository") || msg.includes("could not find .git")) {
|
|
223
|
+
return createError("NOT_A_GIT_REPO", { path: context.path, originalError: msg });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (msg.includes("nothing to commit") || msg.includes("working tree clean")) {
|
|
227
|
+
return createError("NOTHING_TO_COMMIT", { originalError: msg });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (msg.includes("could not resolve ref") || msg.includes("invalid reference")) {
|
|
231
|
+
return createError("REF_NOT_FOUND", { originalError: msg });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (msg.includes("branch") && (msg.includes("does not exist") || msg.includes("not found"))) {
|
|
235
|
+
return createError("BRANCH_NOT_FOUND", { originalError: msg });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (msg.includes("tag") && (msg.includes("does not exist") || msg.includes("not found"))) {
|
|
239
|
+
return createError("TAG_NOT_FOUND", { originalError: msg });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (msg.includes("no commits") || msg.includes("unknown revision")) {
|
|
243
|
+
return createError("NO_COMMITS", { originalError: msg });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (msg.includes("nada para stash") || msg.includes("nothing to stash") || msg.includes("no local changes")) {
|
|
247
|
+
return createError("NOTHING_TO_STASH", { originalError: msg });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (msg.includes("stash") && (msg.includes("not found") || msg.includes("não encontrado"))) {
|
|
251
|
+
return createError("STASH_NOT_FOUND", { originalError: msg });
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (msg.includes("author") || msg.includes("committer")) {
|
|
255
|
+
return createError("VALIDATION_ERROR", {
|
|
256
|
+
message: "Configure user.name e user.email",
|
|
257
|
+
suggestion: "Use git-config para configurar ou forneça tokens de provider",
|
|
258
|
+
originalError: msg
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Generic fallback
|
|
263
|
+
return createError("UNKNOWN_ERROR", { originalError: msg, context });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Resultado de sucesso
|
|
9
267
|
export function asToolResult(result) {
|
|
10
268
|
return {
|
|
11
269
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
@@ -13,10 +271,27 @@ export function asToolResult(result) {
|
|
|
13
271
|
};
|
|
14
272
|
}
|
|
15
273
|
|
|
274
|
+
// Resultado de erro (formato melhorado para AI agents)
|
|
16
275
|
export function asToolError(code, message, data) {
|
|
276
|
+
const errorInfo = ERROR_CODES[code] || {};
|
|
277
|
+
const response = {
|
|
278
|
+
error: true,
|
|
279
|
+
code: code,
|
|
280
|
+
message: message,
|
|
281
|
+
suggestion: errorInfo.suggestion || "Verifique os parâmetros e tente novamente",
|
|
282
|
+
...data
|
|
283
|
+
};
|
|
17
284
|
return {
|
|
18
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
285
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
|
|
19
286
|
isError: true,
|
|
20
287
|
};
|
|
21
288
|
}
|
|
22
289
|
|
|
290
|
+
// Helper para converter MCPError em resposta
|
|
291
|
+
export function errorToResponse(error) {
|
|
292
|
+
if (error instanceof MCPError) {
|
|
293
|
+
return asToolError(error.code, error.message, error.data);
|
|
294
|
+
}
|
|
295
|
+
const mapped = mapExternalError(error);
|
|
296
|
+
return asToolError(mapped.code, mapped.message, mapped.data);
|
|
297
|
+
}
|
package/src/utils/gitAdapter.js
CHANGED
|
@@ -2,8 +2,13 @@ import * as git from "isomorphic-git";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import http from "isomorphic-git/http/node";
|
|
5
|
-
import { MCPError } from "./errors.js";
|
|
5
|
+
import { MCPError, createError, mapExternalError } from "./errors.js";
|
|
6
6
|
import { getProvidersEnv } from "./repoHelpers.js";
|
|
7
|
+
import { withRetry } from "./retry.js";
|
|
8
|
+
|
|
9
|
+
// #region agent log
|
|
10
|
+
const debugLog = (loc, msg, data) => { fetch('http://127.0.0.1:8242/ingest/e5799a4a-1a0d-4201-a6ce-42835e6f6fc7',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:loc,message:msg,data,timestamp:Date.now(),sessionId:'debug-session'})}).catch(()=>{}); };
|
|
11
|
+
// #endregion
|
|
7
12
|
|
|
8
13
|
export class GitAdapter {
|
|
9
14
|
constructor(providerManager) {
|
|
@@ -15,7 +20,14 @@ export class GitAdapter {
|
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
async status(dir) {
|
|
23
|
+
// #region agent log
|
|
24
|
+
const t0 = Date.now();
|
|
25
|
+
debugLog('gitAdapter.js:status', 'START statusMatrix', { dir, hypothesisId: 'A' });
|
|
26
|
+
// #endregion
|
|
18
27
|
const matrix = await git.statusMatrix({ fs, dir });
|
|
28
|
+
// #region agent log
|
|
29
|
+
debugLog('gitAdapter.js:status', 'END statusMatrix', { duration: Date.now() - t0, fileCount: matrix.length, hypothesisId: 'A' });
|
|
30
|
+
// #endregion
|
|
19
31
|
const FILE = 0, HEAD = 1, WORKDIR = 2, STAGE = 3;
|
|
20
32
|
const modified = [], created = [], deleted = [], not_added = [], files = [];
|
|
21
33
|
for (const row of matrix) {
|
|
@@ -90,27 +102,58 @@ export class GitAdapter {
|
|
|
90
102
|
getAuth(remoteUrl) {
|
|
91
103
|
const { githubToken, giteaToken } = getProvidersEnv();
|
|
92
104
|
if (remoteUrl.includes("github.com")) {
|
|
93
|
-
if (!githubToken) throw
|
|
105
|
+
if (!githubToken) throw createError("AUTH_GITHUB_INVALID", { message: "GITHUB_TOKEN não configurado" });
|
|
94
106
|
return () => ({ username: githubToken, password: "x-oauth-basic" });
|
|
95
107
|
}
|
|
96
108
|
if (giteaToken) return () => ({ username: "git", password: giteaToken });
|
|
97
|
-
throw
|
|
109
|
+
throw createError("AUTH_GITEA_INVALID", { message: "GITEA_TOKEN não configurado" });
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
async pushOne(dir, remote, branch, force = false) {
|
|
101
113
|
const remotes = await git.listRemotes({ fs, dir });
|
|
102
114
|
const info = remotes.find(r => r.remote === remote);
|
|
103
|
-
if (!info)
|
|
115
|
+
if (!info) {
|
|
116
|
+
const available = remotes.map(r => r.remote).join(", ");
|
|
117
|
+
throw createError("REMOTE_NOT_FOUND", {
|
|
118
|
+
message: `Remote '${remote}' não encontrado`,
|
|
119
|
+
remote,
|
|
120
|
+
availableRemotes: available || "nenhum"
|
|
121
|
+
});
|
|
122
|
+
}
|
|
104
123
|
const onAuth = this.getAuth(info.url);
|
|
105
124
|
const ref = branch.startsWith("refs/") ? branch : `refs/heads/${branch}`;
|
|
106
|
-
|
|
125
|
+
try {
|
|
126
|
+
await withRetry(() => git.push({ fs, http, dir, remote, ref, remoteRef: ref, onAuth, force }));
|
|
127
|
+
} catch (e) {
|
|
128
|
+
if (e.message?.includes("non-fast-forward") || e.message?.includes("rejected")) {
|
|
129
|
+
throw createError("PUSH_REJECTED", { message: e.message, remote, branch });
|
|
130
|
+
}
|
|
131
|
+
throw mapExternalError(e, { type: "push", remote, branch });
|
|
132
|
+
}
|
|
107
133
|
}
|
|
108
134
|
|
|
109
135
|
async pushParallel(dir, branch, force = false) {
|
|
110
136
|
const remotes = await git.listRemotes({ fs, dir });
|
|
111
137
|
const targets = remotes.filter(r => ["github", "gitea"].includes(r.remote));
|
|
112
|
-
if (targets.length === 0)
|
|
113
|
-
|
|
138
|
+
if (targets.length === 0) {
|
|
139
|
+
throw createError("REMOTE_NOT_FOUND", {
|
|
140
|
+
message: "Nenhum remote github/gitea configurado",
|
|
141
|
+
availableRemotes: remotes.map(r => r.remote).join(", ") || "nenhum",
|
|
142
|
+
suggestion: "Use action='ensure-remotes' para configurar os remotes"
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const results = await Promise.allSettled(targets.map(t => this.pushOne(dir, t.remote, branch, force)));
|
|
146
|
+
const errors = results.filter(r => r.status === "rejected");
|
|
147
|
+
if (errors.length === targets.length) {
|
|
148
|
+
throw createError("PUSH_REJECTED", {
|
|
149
|
+
message: "Push falhou para todos os remotes",
|
|
150
|
+
errors: errors.map(e => e.reason?.message || String(e.reason))
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
pushed: targets.filter((t, i) => results[i].status === "fulfilled").map(t => t.remote),
|
|
155
|
+
failed: errors.map((e, i) => ({ remote: targets[i]?.remote, error: e.reason?.message }))
|
|
156
|
+
};
|
|
114
157
|
}
|
|
115
158
|
|
|
116
159
|
// ============ BRANCHES ============
|
|
@@ -188,7 +231,12 @@ export class GitAdapter {
|
|
|
188
231
|
|
|
189
232
|
async saveStash(dir, message = "WIP", includeUntracked = false) {
|
|
190
233
|
const st = await this.status(dir);
|
|
191
|
-
if (st.isClean && !includeUntracked)
|
|
234
|
+
if (st.isClean && !includeUntracked) {
|
|
235
|
+
throw createError("NOTHING_TO_STASH", {
|
|
236
|
+
message: "Working tree limpa, nada para stash",
|
|
237
|
+
status: st
|
|
238
|
+
});
|
|
239
|
+
}
|
|
192
240
|
|
|
193
241
|
// Salva arquivos modificados/adicionados
|
|
194
242
|
const filesToSave = [...st.not_added, ...st.modified, ...st.created];
|
|
@@ -223,7 +271,14 @@ export class GitAdapter {
|
|
|
223
271
|
const stashes = await this.listStash(dir);
|
|
224
272
|
const idx = Number(ref.match(/\{(\d+)\}/)?.[1] || 0);
|
|
225
273
|
const stash = stashes[idx];
|
|
226
|
-
if (!stash)
|
|
274
|
+
if (!stash) {
|
|
275
|
+
throw createError("STASH_NOT_FOUND", {
|
|
276
|
+
message: `Stash '${ref}' não encontrado`,
|
|
277
|
+
requestedIndex: idx,
|
|
278
|
+
availableStashes: stashes.length,
|
|
279
|
+
stashList: stashes.map((s, i) => `stash@{${i}}: ${s.message}`).slice(0, 5)
|
|
280
|
+
});
|
|
281
|
+
}
|
|
227
282
|
|
|
228
283
|
// Restaura arquivos do stash
|
|
229
284
|
for (const [file, content] of Object.entries(stash.files)) {
|
|
@@ -330,17 +385,39 @@ export class GitAdapter {
|
|
|
330
385
|
async fetch(dir, remote, branch) {
|
|
331
386
|
const remotes = await git.listRemotes({ fs, dir });
|
|
332
387
|
const info = remotes.find(r => r.remote === remote);
|
|
333
|
-
if (!info)
|
|
388
|
+
if (!info) {
|
|
389
|
+
throw createError("REMOTE_NOT_FOUND", {
|
|
390
|
+
message: `Remote '${remote}' não encontrado`,
|
|
391
|
+
availableRemotes: remotes.map(r => r.remote)
|
|
392
|
+
});
|
|
393
|
+
}
|
|
334
394
|
const onAuth = this.getAuth(info.url);
|
|
335
|
-
|
|
395
|
+
try {
|
|
396
|
+
await withRetry(() => git.fetch({ fs, http, dir, remote, ref: branch, singleBranch: true, onAuth }));
|
|
397
|
+
} catch (e) {
|
|
398
|
+
throw mapExternalError(e, { type: "fetch", remote, branch, provider: info.url.includes("github") ? "github" : "gitea" });
|
|
399
|
+
}
|
|
336
400
|
}
|
|
337
401
|
|
|
338
402
|
async pull(dir, remote, branch) {
|
|
339
403
|
const remotes = await git.listRemotes({ fs, dir });
|
|
340
404
|
const info = remotes.find(r => r.remote === remote);
|
|
341
|
-
if (!info)
|
|
405
|
+
if (!info) {
|
|
406
|
+
throw createError("REMOTE_NOT_FOUND", {
|
|
407
|
+
message: `Remote '${remote}' não encontrado`,
|
|
408
|
+
availableRemotes: remotes.map(r => r.remote)
|
|
409
|
+
});
|
|
410
|
+
}
|
|
342
411
|
const onAuth = this.getAuth(info.url);
|
|
343
|
-
|
|
412
|
+
const author = await this.getAuthor(dir);
|
|
413
|
+
try {
|
|
414
|
+
await withRetry(async () => git.pull({ fs, http, dir, remote, ref: branch, singleBranch: true, onAuth, author }));
|
|
415
|
+
} catch (e) {
|
|
416
|
+
if (e.message?.includes("conflict")) {
|
|
417
|
+
throw createError("MERGE_CONFLICT", { message: e.message, remote, branch });
|
|
418
|
+
}
|
|
419
|
+
throw mapExternalError(e, { type: "pull", remote, branch, provider: info.url.includes("github") ? "github" : "gitea" });
|
|
420
|
+
}
|
|
344
421
|
}
|
|
345
422
|
|
|
346
423
|
// ============ RESET ============
|
|
@@ -352,10 +429,26 @@ export class GitAdapter {
|
|
|
352
429
|
const steps = parseInt(match[2], 10);
|
|
353
430
|
const commits = await git.log({ fs, dir, ref: baseRef, depth: steps + 1 });
|
|
354
431
|
if (commits.length > steps) return commits[steps].oid;
|
|
355
|
-
throw
|
|
432
|
+
throw createError("INSUFFICIENT_HISTORY", {
|
|
433
|
+
message: `Não foi possível resolver ${ref}`,
|
|
434
|
+
requestedSteps: steps,
|
|
435
|
+
availableCommits: commits.length,
|
|
436
|
+
suggestion: `Histórico tem apenas ${commits.length} commits. Use HEAD~${commits.length - 1} no máximo.`
|
|
437
|
+
});
|
|
356
438
|
}
|
|
357
439
|
// Refs normais
|
|
358
|
-
|
|
440
|
+
try {
|
|
441
|
+
return await git.resolveRef({ fs, dir, ref });
|
|
442
|
+
} catch (e) {
|
|
443
|
+
const branches = await git.listBranches({ fs, dir }).catch(() => []);
|
|
444
|
+
const tags = await git.listTags({ fs, dir }).catch(() => []);
|
|
445
|
+
throw createError("REF_NOT_FOUND", {
|
|
446
|
+
message: `Ref '${ref}' não encontrada`,
|
|
447
|
+
ref,
|
|
448
|
+
availableBranches: branches.slice(0, 10),
|
|
449
|
+
availableTags: tags.slice(0, 10)
|
|
450
|
+
});
|
|
451
|
+
}
|
|
359
452
|
}
|
|
360
453
|
|
|
361
454
|
async resetSoft(dir, ref) {
|
package/src/utils/retry.js
CHANGED
|
@@ -1,12 +1,61 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
// Sistema de Retry com Backoff Exponencial
|
|
2
|
+
|
|
3
|
+
const DEFAULT_OPTIONS = {
|
|
4
|
+
maxRetries: 3,
|
|
5
|
+
initialDelay: 1000,
|
|
6
|
+
maxDelay: 10000,
|
|
7
|
+
backoffFactor: 2,
|
|
8
|
+
retryableErrors: ["ETIMEDOUT", "ECONNRESET", "ENOTFOUND", "ECONNREFUSED", "timeout", "rate limit", "429", "503", "502"]
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function shouldRetry(error, options) {
|
|
12
|
+
const msg = (error?.message || String(error)).toLowerCase();
|
|
13
|
+
const code = error?.code?.toLowerCase() || "";
|
|
14
|
+
|
|
15
|
+
return options.retryableErrors.some(e =>
|
|
16
|
+
msg.includes(e.toLowerCase()) || code.includes(e.toLowerCase())
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sleep(ms) {
|
|
21
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function withRetry(fn, options = {}) {
|
|
25
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
26
|
+
let lastError;
|
|
27
|
+
let delay = opts.initialDelay;
|
|
28
|
+
|
|
29
|
+
for (let attempt = 1; attempt <= opts.maxRetries; attempt++) {
|
|
30
|
+
try {
|
|
31
|
+
return await fn();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
lastError = error;
|
|
34
|
+
|
|
35
|
+
if (attempt === opts.maxRetries || !shouldRetry(error, opts)) {
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Log retry (silencioso)
|
|
40
|
+
console.error(`[Retry] Attempt ${attempt}/${opts.maxRetries} failed, retrying in ${delay}ms...`);
|
|
41
|
+
|
|
42
|
+
await sleep(delay);
|
|
43
|
+
delay = Math.min(delay * opts.backoffFactor, opts.maxDelay);
|
|
44
|
+
}
|
|
9
45
|
}
|
|
10
|
-
|
|
46
|
+
|
|
47
|
+
throw lastError;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Wrapper para axios com retry
|
|
51
|
+
export async function axiosWithRetry(axiosInstance, config, options = {}) {
|
|
52
|
+
return withRetry(() => axiosInstance(config), options);
|
|
11
53
|
}
|
|
12
54
|
|
|
55
|
+
// Wrapper para operações git com retry
|
|
56
|
+
export async function gitWithRetry(fn, options = {}) {
|
|
57
|
+
return withRetry(fn, {
|
|
58
|
+
...options,
|
|
59
|
+
retryableErrors: [...DEFAULT_OPTIONS.retryableErrors, "ENOENT", "lock"]
|
|
60
|
+
});
|
|
61
|
+
}
|