@andrebuzeli/git-mcp 15.9.1 → 15.10.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 +1 -1
- package/src/tools/git-branches.js +19 -1
- package/src/tools/git-clone.js +39 -6
- package/src/tools/git-config.js +4 -4
- package/src/tools/git-diff.js +6 -6
- package/src/tools/git-files.js +1 -1
- package/src/tools/git-history.js +2 -2
- package/src/tools/git-ignore.js +4 -4
- package/src/tools/git-issues.js +7 -7
- package/src/tools/git-merge.js +39 -15
- package/src/tools/git-pulls.js +7 -7
- package/src/tools/git-remote.js +1 -1
- package/src/tools/git-reset.js +5 -5
- package/src/tools/git-stash.js +4 -4
- package/src/tools/git-sync.js +39 -4
- package/src/tools/git-tags.js +5 -5
- package/src/tools/git-workflow.js +66 -158
- package/src/utils/gitAdapter.js +1 -1
- package/src/utils/repoHelpers.js +56 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -47,7 +47,7 @@ if (!hasGitHub && !hasGitea) {
|
|
|
47
47
|
|
|
48
48
|
const transport = new StdioServerTransport();
|
|
49
49
|
const server = new Server(
|
|
50
|
-
{ name: "git-mcpv2", version: "15.
|
|
50
|
+
{ name: "git-mcpv2", version: "15.9.2" },
|
|
51
51
|
{ capabilities: { tools: {}, resources: {}, prompts: {} } }
|
|
52
52
|
);
|
|
53
53
|
server.connect(transport);
|
|
@@ -95,7 +95,25 @@ CONVENÇÕES DE NOMES:
|
|
|
95
95
|
}
|
|
96
96
|
if (action === "checkout") {
|
|
97
97
|
if (!args.branch) return asToolError("MISSING_PARAMETER", "branch é obrigatório para checkout", { parameter: "branch" });
|
|
98
|
-
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await git.checkout(projectPath, args.branch);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
const msg = e.message || "";
|
|
103
|
+
// Auto-fix: Create if not exists (common intent)
|
|
104
|
+
if (msg.includes("did not match any file") || msg.includes("pathspec") || msg.includes("not found")) {
|
|
105
|
+
// We can suggest or auto-create if configured. For now, let's suggest clearly or auto-create if it looks like a feature branch?
|
|
106
|
+
// Safer: Suggest. Or better: check if we should auto-create.
|
|
107
|
+
// Let's stick to safe behavior but enhanced error.
|
|
108
|
+
|
|
109
|
+
// Check if user meant create
|
|
110
|
+
return asToolError("BRANCH_NOT_FOUND", `Branch '${args.branch}' não existe`, {
|
|
111
|
+
suggestion: "Use action='create' para criar esta branch, ou verifique o nome."
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
throw e;
|
|
115
|
+
}
|
|
116
|
+
|
|
99
117
|
return asToolResult({ success: true, branch: args.branch, message: `Mudou para branch '${args.branch}'` });
|
|
100
118
|
}
|
|
101
119
|
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, { availableActions: ["list", "create", "delete", "rename", "checkout"] });
|
package/src/tools/git-clone.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import { asToolError, asToolResult, errorToResponse, createError } from "../utils/errors.js";
|
|
3
|
-
import { validateProjectPath } from "../utils/repoHelpers.js";
|
|
3
|
+
import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
4
4
|
|
|
5
5
|
const ajv = new Ajv({ allErrors: true });
|
|
6
6
|
|
|
@@ -75,11 +75,44 @@ NOTAS:
|
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
// Idempotency check
|
|
79
|
+
if (fs.existsSync(path.join(projectPath, ".git"))) {
|
|
80
|
+
try {
|
|
81
|
+
// Check if it's the same repo
|
|
82
|
+
// git.getRemoteUrl is not standard in adapter, use exec or listRemotes logic
|
|
83
|
+
// Assuming git.listRemotes or similar exists, or we catch the error
|
|
84
|
+
// Let's try to infer from config or just warn
|
|
85
|
+
|
|
86
|
+
// We'll rely on git status to check if it's healthy
|
|
87
|
+
await git.status(projectPath);
|
|
88
|
+
|
|
89
|
+
return asToolResult({
|
|
90
|
+
success: true,
|
|
91
|
+
url,
|
|
92
|
+
path: projectPath,
|
|
93
|
+
branch: args.branch || "current",
|
|
94
|
+
message: `Repositório já existe em '${projectPath}'. Clone ignorado (idempotente).`,
|
|
95
|
+
nextStep: "Use git-workflow status para ver o estado atual"
|
|
96
|
+
});
|
|
97
|
+
} catch (e) {
|
|
98
|
+
// If status fails, maybe it's broken
|
|
99
|
+
console.warn("Existing repo check failed:", e);
|
|
100
|
+
}
|
|
101
|
+
} else if (fs.existsSync(projectPath) && fs.readdirSync(projectPath).length > 0) {
|
|
102
|
+
return asToolError("DIR_NOT_EMPTY", `Diretório '${projectPath}' existe e não está vazio`, {
|
|
103
|
+
suggestion: "Use um diretório novo ou limpe o atual"
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const result = await withRetry(
|
|
108
|
+
() => git.clone(url, projectPath, {
|
|
109
|
+
branch: args.branch,
|
|
110
|
+
depth: args.depth,
|
|
111
|
+
singleBranch: args.singleBranch
|
|
112
|
+
}),
|
|
113
|
+
3,
|
|
114
|
+
"clone"
|
|
115
|
+
);
|
|
83
116
|
|
|
84
117
|
return asToolResult({
|
|
85
118
|
success: true,
|
package/src/tools/git-config.js
CHANGED
|
@@ -66,21 +66,21 @@ EXEMPLOS:
|
|
|
66
66
|
validateProjectPath(projectPath);
|
|
67
67
|
if (action === "get") {
|
|
68
68
|
if (!args.key) return asToolError("MISSING_PARAMETER", "key é obrigatório", { parameter: "key" });
|
|
69
|
-
const val = await git.getConfig(projectPath, args.key, scope);
|
|
69
|
+
const val = await withRetry(() => git.getConfig(projectPath, args.key, scope), 3, "get-config");
|
|
70
70
|
return asToolResult({ key: args.key, value: val, found: val !== undefined, scope });
|
|
71
71
|
}
|
|
72
72
|
if (action === "set") {
|
|
73
73
|
if (!args.key) return asToolError("MISSING_PARAMETER", "key é obrigatório", { parameter: "key" });
|
|
74
|
-
await git.setConfig(projectPath, args.key, args.value ?? "", scope);
|
|
74
|
+
await withRetry(() => git.setConfig(projectPath, args.key, args.value ?? "", scope), 3, "set-config");
|
|
75
75
|
return asToolResult({ success: true, key: args.key, value: args.value ?? "", scope });
|
|
76
76
|
}
|
|
77
77
|
if (action === "unset") {
|
|
78
78
|
if (!args.key) return asToolError("MISSING_PARAMETER", "key é obrigatório", { parameter: "key" });
|
|
79
|
-
await git.unsetConfig(projectPath, args.key, scope);
|
|
79
|
+
await withRetry(() => git.unsetConfig(projectPath, args.key, scope), 3, "unset-config");
|
|
80
80
|
return asToolResult({ success: true, key: args.key, removed: true, scope });
|
|
81
81
|
}
|
|
82
82
|
if (action === "list") {
|
|
83
|
-
const items = await git.listConfig(projectPath, scope);
|
|
83
|
+
const items = await withRetry(() => git.listConfig(projectPath, scope), 3, "list-config");
|
|
84
84
|
return asToolResult({ scope, configs: items, count: Object.keys(items).length });
|
|
85
85
|
}
|
|
86
86
|
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, { availableActions: ["get", "set", "unset", "list"] });
|
package/src/tools/git-diff.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
|
-
import { validateProjectPath } from "../utils/repoHelpers.js";
|
|
3
|
+
import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
4
4
|
|
|
5
5
|
const ajv = new Ajv({ allErrors: true });
|
|
6
6
|
|
|
@@ -80,10 +80,10 @@ EXEMPLOS:
|
|
|
80
80
|
try {
|
|
81
81
|
validateProjectPath(projectPath);
|
|
82
82
|
if (action === "show") {
|
|
83
|
-
const diff = await git.diff(projectPath, {
|
|
83
|
+
const diff = await withRetry(() => git.diff(projectPath, {
|
|
84
84
|
file: fileParam,
|
|
85
85
|
context: args.context || 3
|
|
86
|
-
});
|
|
86
|
+
}), 3, "diff-show");
|
|
87
87
|
|
|
88
88
|
if (!diff || diff.length === 0) {
|
|
89
89
|
return asToolResult({
|
|
@@ -100,10 +100,10 @@ EXEMPLOS:
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
if (action === "compare") {
|
|
103
|
-
const diff = await git.diffCommits(projectPath, fromParam, toParam, {
|
|
103
|
+
const diff = await withRetry(() => git.diffCommits(projectPath, fromParam, toParam, {
|
|
104
104
|
file: fileParam,
|
|
105
105
|
context: args.context || 3
|
|
106
|
-
});
|
|
106
|
+
}), 3, "diff-compare");
|
|
107
107
|
|
|
108
108
|
return asToolResult({
|
|
109
109
|
from: fromParam,
|
|
@@ -115,7 +115,7 @@ EXEMPLOS:
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
if (action === "stat") {
|
|
118
|
-
const stats = await git.diffStats(projectPath, fromParam, toParam);
|
|
118
|
+
const stats = await withRetry(() => git.diffStats(projectPath, fromParam, toParam), 3, "diff-stat");
|
|
119
119
|
|
|
120
120
|
return asToolResult({
|
|
121
121
|
from: fromParam,
|
package/src/tools/git-files.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
|
-
import { validateProjectPath } from "../utils/repoHelpers.js";
|
|
3
|
+
import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
4
4
|
|
|
5
5
|
const ajv = new Ajv({ allErrors: true });
|
|
6
6
|
|
package/src/tools/git-history.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
|
-
import { validateProjectPath } from "../utils/repoHelpers.js";
|
|
3
|
+
import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
4
4
|
|
|
5
5
|
const ajv = new Ajv({ allErrors: true });
|
|
6
6
|
|
|
@@ -58,7 +58,7 @@ EXEMPLOS:
|
|
|
58
58
|
try {
|
|
59
59
|
validateProjectPath(projectPath);
|
|
60
60
|
if (action === "log") {
|
|
61
|
-
const items = await git.log(projectPath, { ref: args.ref || "HEAD", maxCount: args.maxCount || 50 });
|
|
61
|
+
const items = await withRetry(() => git.log(projectPath, { ref: args.ref || "HEAD", maxCount: args.maxCount || 50 }), 3, "history-log");
|
|
62
62
|
if (items.length === 0) {
|
|
63
63
|
return asToolResult({
|
|
64
64
|
commits: [],
|
package/src/tools/git-ignore.js
CHANGED
|
@@ -61,7 +61,7 @@ EXEMPLOS:
|
|
|
61
61
|
try {
|
|
62
62
|
validateProjectPath(projectPath);
|
|
63
63
|
if (action === "list") {
|
|
64
|
-
const items = await git.listGitignore(projectPath);
|
|
64
|
+
const items = await withRetry(() => git.listGitignore(projectPath), 3, "ignore-list");
|
|
65
65
|
return asToolResult({ patterns: items, count: items.length, hasGitignore: items.length > 0 || true });
|
|
66
66
|
}
|
|
67
67
|
if (action === "create") {
|
|
@@ -71,21 +71,21 @@ EXEMPLOS:
|
|
|
71
71
|
suggestion: "Forneça um array de padrões. Ex: ['node_modules/', '*.log', '.env']"
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
-
await git.createGitignore(projectPath, patterns);
|
|
74
|
+
await withRetry(() => git.createGitignore(projectPath, patterns), 3, "ignore-create");
|
|
75
75
|
return asToolResult({ success: true, patterns, message: ".gitignore criado" });
|
|
76
76
|
}
|
|
77
77
|
if (action === "add") {
|
|
78
78
|
if (patterns.length === 0) {
|
|
79
79
|
return asToolError("MISSING_PARAMETER", "patterns é obrigatório", { parameter: "patterns" });
|
|
80
80
|
}
|
|
81
|
-
await git.addToGitignore(projectPath, patterns);
|
|
81
|
+
await withRetry(() => git.addToGitignore(projectPath, patterns), 3, "ignore-add");
|
|
82
82
|
return asToolResult({ success: true, added: patterns });
|
|
83
83
|
}
|
|
84
84
|
if (action === "remove") {
|
|
85
85
|
if (patterns.length === 0) {
|
|
86
86
|
return asToolError("MISSING_PARAMETER", "patterns é obrigatório", { parameter: "patterns" });
|
|
87
87
|
}
|
|
88
|
-
await git.removeFromGitignore(projectPath, patterns);
|
|
88
|
+
await withRetry(() => git.removeFromGitignore(projectPath, patterns), 3, "ignore-remove");
|
|
89
89
|
return asToolResult({ success: true, removed: patterns });
|
|
90
90
|
}
|
|
91
91
|
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, { availableActions: ["list", "create", "add", "remove"] });
|
package/src/tools/git-issues.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import axios from "axios";
|
|
3
3
|
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
4
|
-
import { getRepoNameFromPath, validateProjectPath } from "../utils/repoHelpers.js";
|
|
4
|
+
import { getRepoNameFromPath, validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
5
5
|
import { runBoth } from "../utils/providerExec.js";
|
|
6
6
|
|
|
7
7
|
const ajv = new Ajv({ allErrors: true });
|
|
@@ -65,26 +65,26 @@ BOAS PRÁTICAS:
|
|
|
65
65
|
try {
|
|
66
66
|
if (args.action === "create") {
|
|
67
67
|
if (!args.title) return asToolError("MISSING_PARAMETER", "title é obrigatório para criar issue", { parameter: "title" });
|
|
68
|
-
const out = await runBoth(pm, {
|
|
68
|
+
const out = await withRetry(() => runBoth(pm, {
|
|
69
69
|
github: async (owner) => { const r = await pm.github.rest.issues.create({ owner, repo, title: args.title, body: args.body || "" }); return { ok: true, number: r.data.number, url: r.data.html_url }; },
|
|
70
70
|
gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${base}/api/v1/repos/${owner}/${repo}/issues`, { title: args.title, body: args.body || "" }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, number: r.data?.number }; }
|
|
71
|
-
});
|
|
71
|
+
}), 3, "issues-create");
|
|
72
72
|
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), title: args.title, providers: out });
|
|
73
73
|
}
|
|
74
74
|
if (args.action === "list") {
|
|
75
|
-
const out = await runBoth(pm, {
|
|
75
|
+
const out = await withRetry(() => runBoth(pm, {
|
|
76
76
|
github: async (owner) => { const r = await pm.github.rest.issues.listForRepo({ owner, repo, state: "all", per_page: 30 }); return { ok: true, count: r.data.length, issues: r.data.slice(0, 10).map(i => ({ number: i.number, title: i.title, state: i.state })) }; },
|
|
77
77
|
gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${base}/api/v1/repos/${owner}/${repo}/issues?state=all`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data||[]).length, issues: (r.data||[]).slice(0, 10).map(i => ({ number: i.number, title: i.title, state: i.state })) }; }
|
|
78
|
-
});
|
|
78
|
+
}), 3, "issues-list");
|
|
79
79
|
return asToolResult({ providers: out });
|
|
80
80
|
}
|
|
81
81
|
if (args.action === "comment") {
|
|
82
82
|
if (!args.number) return asToolError("MISSING_PARAMETER", "number é obrigatório para comentar", { parameter: "number" });
|
|
83
83
|
if (!args.body) return asToolError("MISSING_PARAMETER", "body é obrigatório para comentar", { parameter: "body" });
|
|
84
|
-
const out = await runBoth(pm, {
|
|
84
|
+
const out = await withRetry(() => runBoth(pm, {
|
|
85
85
|
github: async (owner) => { await pm.github.rest.issues.createComment({ owner, repo, issue_number: args.number, body: args.body }); return { ok: true }; },
|
|
86
86
|
gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); await axios.post(`${base}/api/v1/repos/${owner}/${repo}/issues/${args.number}/comments`, { body: args.body }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
|
|
87
|
-
});
|
|
87
|
+
}), 3, "issues-comment");
|
|
88
88
|
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), issueNumber: args.number, providers: out });
|
|
89
89
|
}
|
|
90
90
|
return asToolError("VALIDATION_ERROR", `Ação '${args.action}' não suportada`, { availableActions: ["create", "list", "comment"] });
|
package/src/tools/git-merge.js
CHANGED
|
@@ -91,6 +91,16 @@ SQUASH:
|
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// Pre-merge check: working tree status
|
|
95
|
+
const status = await git.status(projectPath);
|
|
96
|
+
if (!status.isClean) {
|
|
97
|
+
// We could warn or fail. Standard git refuses merge if changes would be overwritten.
|
|
98
|
+
// Let's just warn in logs or return error if critical?
|
|
99
|
+
// Git will fail anyway if conflicts with local changes.
|
|
100
|
+
// Let's inform the user.
|
|
101
|
+
console.warn("[GitMerge] Working tree not clean. Merge might fail.");
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
const currentBranch = await git.getCurrentBranch(projectPath);
|
|
95
105
|
if (currentBranch === args.branch) {
|
|
96
106
|
return asToolError("VALIDATION_ERROR", "Não pode fazer merge de uma branch nela mesma", {
|
|
@@ -99,21 +109,35 @@ SQUASH:
|
|
|
99
109
|
});
|
|
100
110
|
}
|
|
101
111
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
try {
|
|
113
|
+
const result = await git.merge(projectPath, args.branch, {
|
|
114
|
+
message: args.message,
|
|
115
|
+
noCommit: args.noCommit,
|
|
116
|
+
squash: args.squash
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return asToolResult({
|
|
120
|
+
success: true,
|
|
121
|
+
from: args.branch,
|
|
122
|
+
into: currentBranch,
|
|
123
|
+
...result,
|
|
124
|
+
message: result.conflicts?.length > 0
|
|
125
|
+
? `Merge com conflitos. Resolva os conflitos e faça commit.`
|
|
126
|
+
: `Merge de '${args.branch}' em '${currentBranch}' concluído`
|
|
127
|
+
});
|
|
128
|
+
} catch (e) {
|
|
129
|
+
const msg = e.message || "";
|
|
130
|
+
if (msg.includes("conflict") || msg.includes("CONFLICT")) {
|
|
131
|
+
return asToolResult({
|
|
132
|
+
success: false,
|
|
133
|
+
conflict: true,
|
|
134
|
+
message: "Merge resultou em conflitos. Resolva manualmente e faça commit.",
|
|
135
|
+
files: [], // If we could parse files from error, great.
|
|
136
|
+
nextStep: "Edite os arquivos conflitantes, use git add e git commit."
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
throw e;
|
|
140
|
+
}
|
|
117
141
|
}
|
|
118
142
|
|
|
119
143
|
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
|
package/src/tools/git-pulls.js
CHANGED
|
@@ -80,26 +80,26 @@ NOTA: O PR é criado em AMBOS os providers simultaneamente.`;
|
|
|
80
80
|
const base = args.base;
|
|
81
81
|
const title = args.title || `${head} -> ${base}`;
|
|
82
82
|
const body = args.body || "";
|
|
83
|
-
const out = await runBoth(pm, {
|
|
83
|
+
const out = await withRetry(() => runBoth(pm, {
|
|
84
84
|
github: async (owner) => { const r = await pm.github.rest.pulls.create({ owner, repo, title, head, base, body }); return { ok: true, number: r.data.number, url: r.data.html_url }; },
|
|
85
85
|
gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls`, { title, head, base, body }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, number: r.data?.number }; }
|
|
86
|
-
});
|
|
86
|
+
}), 3, "pulls-create");
|
|
87
87
|
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), title, head, base, providers: out });
|
|
88
88
|
}
|
|
89
89
|
if (args.action === "list") {
|
|
90
|
-
const out = await runBoth(pm, {
|
|
90
|
+
const out = await withRetry(() => runBoth(pm, {
|
|
91
91
|
github: async (owner) => { const r = await pm.github.rest.pulls.list({ owner, repo, state: "all", per_page: 30 }); return { ok: true, count: r.data.length, pulls: r.data.slice(0, 10).map(p => ({ number: p.number, title: p.title, state: p.state, head: p.head.ref, base: p.base.ref })) }; },
|
|
92
92
|
gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls?state=all`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data||[]).length, pulls: (r.data||[]).slice(0, 10).map(p => ({ number: p.number, title: p.title, state: p.state, head: p.head?.ref, base: p.base?.ref })) }; }
|
|
93
|
-
});
|
|
93
|
+
}), 3, "pulls-list");
|
|
94
94
|
return asToolResult({ providers: out });
|
|
95
95
|
}
|
|
96
96
|
if (args.action === "files") {
|
|
97
97
|
if (!args.number) return asToolError("MISSING_PARAMETER", "number é obrigatório para ver arquivos do PR", { parameter: "number" });
|
|
98
|
-
const out = await runBoth(pm, {
|
|
98
|
+
const out = await withRetry(() => runBoth(pm, {
|
|
99
99
|
github: async (owner) => { const r = await pm.github.rest.pulls.listFiles({ owner, repo, pull_number: args.number }); return { ok: true, count: r.data.length, files: r.data.map(f => ({ filename: f.filename, status: f.status, additions: f.additions, deletions: f.deletions })) }; },
|
|
100
100
|
gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls/${args.number}/files`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data||[]).length, files: (r.data||[]).map(f => ({ filename: f.filename, status: f.status })) }; }
|
|
101
|
-
});
|
|
102
|
-
return asToolResult({
|
|
101
|
+
}), 3, "pulls-files");
|
|
102
|
+
return asToolResult({ success: true, providers: out });
|
|
103
103
|
}
|
|
104
104
|
return asToolError("VALIDATION_ERROR", `Ação '${args.action}' não suportada`, { availableActions: ["create", "list", "files"] });
|
|
105
105
|
} catch (e) {
|
package/src/tools/git-remote.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import archiver from "archiver";
|
|
6
6
|
import { asToolError, asToolResult, errorToResponse, mapExternalError } from "../utils/errors.js";
|
|
7
|
-
import { getRepoNameFromPath, validateProjectPath } from "../utils/repoHelpers.js";
|
|
7
|
+
import { getRepoNameFromPath, validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
8
8
|
import { runBoth } from "../utils/providerExec.js";
|
|
9
9
|
|
|
10
10
|
/**
|
package/src/tools/git-reset.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
|
-
import { validateProjectPath } from "../utils/repoHelpers.js";
|
|
3
|
+
import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
4
4
|
|
|
5
5
|
const ajv = new Ajv({ allErrors: true });
|
|
6
6
|
|
|
@@ -73,19 +73,19 @@ REFERÊNCIAS:
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
if (action === "soft") {
|
|
76
|
-
await git.resetSoft(projectPath, ref);
|
|
76
|
+
await withRetry(() => git.resetSoft(projectPath, ref), 3, "reset-soft");
|
|
77
77
|
return asToolResult({ success: true, action: "soft", ref, message: "Commits desfeitos, mudanças mantidas staged" });
|
|
78
78
|
}
|
|
79
79
|
if (action === "mixed") {
|
|
80
|
-
await git.resetMixed(projectPath, ref);
|
|
80
|
+
await withRetry(() => git.resetMixed(projectPath, ref), 3, "reset-mixed");
|
|
81
81
|
return asToolResult({ success: true, action: "mixed", ref, message: "Commits desfeitos, mudanças mantidas no working directory" });
|
|
82
82
|
}
|
|
83
83
|
if (action === "hard") {
|
|
84
|
-
await git.resetHard(projectPath, ref);
|
|
84
|
+
await withRetry(() => git.resetHard(projectPath, ref), 3, "reset-hard");
|
|
85
85
|
return asToolResult({ success: true, action: "hard", ref, message: "⚠️ Reset hard executado - mudanças descartadas" });
|
|
86
86
|
}
|
|
87
87
|
if (action === "hard-clean") {
|
|
88
|
-
const result = await git.resetHardClean(projectPath, ref);
|
|
88
|
+
const result = await withRetry(() => git.resetHardClean(projectPath, ref), 3, "reset-hard-clean");
|
|
89
89
|
return asToolResult({
|
|
90
90
|
success: true,
|
|
91
91
|
action: "hard-clean",
|
package/src/tools/git-stash.js
CHANGED
|
@@ -67,7 +67,7 @@ AÇÕES:
|
|
|
67
67
|
try {
|
|
68
68
|
validateProjectPath(projectPath);
|
|
69
69
|
if (action === "list") {
|
|
70
|
-
const items = await git.listStash(projectPath);
|
|
70
|
+
const items = await withRetry(() => git.listStash(projectPath), 3, "stash-list");
|
|
71
71
|
return asToolResult({
|
|
72
72
|
stashes: items.map((s, i) => ({ index: i, ref: `stash@{${i}}`, message: s.message, timestamp: s.timestamp })),
|
|
73
73
|
count: items.length,
|
|
@@ -79,7 +79,7 @@ AÇÕES:
|
|
|
79
79
|
if (status.isClean && !args.includeUntracked) {
|
|
80
80
|
return asToolError("NOTHING_TO_STASH", "Working tree limpa, nada para stash", { status });
|
|
81
81
|
}
|
|
82
|
-
await git.saveStash(projectPath, args.message || "WIP", !!args.includeUntracked);
|
|
82
|
+
await withRetry(() => git.saveStash(projectPath, args.message || "WIP", !!args.includeUntracked), 3, "stash-save");
|
|
83
83
|
return asToolResult({ success: true, message: args.message || "WIP", savedFiles: status.modified.length + status.created.length + status.not_added.length });
|
|
84
84
|
}
|
|
85
85
|
if (action === "apply") {
|
|
@@ -87,7 +87,7 @@ AÇÕES:
|
|
|
87
87
|
if (items.length === 0) {
|
|
88
88
|
return asToolError("STASH_NOT_FOUND", "Nenhum stash disponível", { availableStashes: [] });
|
|
89
89
|
}
|
|
90
|
-
await git.applyStash(projectPath, args.ref || "stash@{0}");
|
|
90
|
+
await withRetry(() => git.applyStash(projectPath, args.ref || "stash@{0}"), 3, "stash-apply");
|
|
91
91
|
return asToolResult({ success: true, ref: args.ref || "stash@{0}", message: "Stash aplicado (ainda está na lista)" });
|
|
92
92
|
}
|
|
93
93
|
if (action === "pop") {
|
|
@@ -95,7 +95,7 @@ AÇÕES:
|
|
|
95
95
|
if (items.length === 0) {
|
|
96
96
|
return asToolError("STASH_NOT_FOUND", "Nenhum stash disponível", { availableStashes: [] });
|
|
97
97
|
}
|
|
98
|
-
await git.popStash(projectPath, args.ref || "stash@{0}");
|
|
98
|
+
await withRetry(() => git.popStash(projectPath, args.ref || "stash@{0}"), 3, "stash-pop");
|
|
99
99
|
return asToolResult({ success: true, ref: args.ref || "stash@{0}", message: "Stash aplicado e removido da lista" });
|
|
100
100
|
}
|
|
101
101
|
if (action === "drop") {
|
package/src/tools/git-sync.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
|
-
import { validateProjectPath } from "../utils/repoHelpers.js";
|
|
3
|
+
import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
4
4
|
|
|
5
5
|
const ajv = new Ajv({ allErrors: true });
|
|
6
6
|
|
|
@@ -58,7 +58,11 @@ NOTA: Se pull falhar com conflito, resolva manualmente e faça commit.`;
|
|
|
58
58
|
const branch = args.branch || await git.getCurrentBranch(projectPath);
|
|
59
59
|
if (action === "fetch") {
|
|
60
60
|
const remotes = args.remote ? [args.remote] : ["origin", "github", "gitea"];
|
|
61
|
-
const promises = remotes.map(r =>
|
|
61
|
+
const promises = remotes.map(r => withRetry(
|
|
62
|
+
() => git.fetch(projectPath, r, branch),
|
|
63
|
+
3,
|
|
64
|
+
`fetch-${r}`
|
|
65
|
+
).then(() => ({ remote: r, ok: true })).catch(e => ({ remote: r, ok: false, error: String(e?.message || e) })));
|
|
62
66
|
const results = await Promise.all(promises);
|
|
63
67
|
const successful = results.filter(x => x.ok);
|
|
64
68
|
const failed = results.filter(x => !x.ok);
|
|
@@ -71,17 +75,48 @@ NOTA: Se pull falhar com conflito, resolva manualmente e faça commit.`;
|
|
|
71
75
|
});
|
|
72
76
|
}
|
|
73
77
|
if (action === "pull") {
|
|
78
|
+
// Auto-stash logic
|
|
79
|
+
const status = await git.status(projectPath);
|
|
80
|
+
let stashed = false;
|
|
81
|
+
if (!status.isClean) {
|
|
82
|
+
console.log("[GitSync] Working directory dirty, creating auto-stash...");
|
|
83
|
+
try {
|
|
84
|
+
await git.saveStash(projectPath, `Auto-stash before pull ${new Date().toISOString()}`, true);
|
|
85
|
+
stashed = true;
|
|
86
|
+
} catch (stashError) {
|
|
87
|
+
return asToolError("STASH_FAILED", "Falha ao criar stash automático antes do pull", { error: stashError.message });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
74
91
|
const remotes = args.remote ? [args.remote] : ["origin", "github", "gitea"];
|
|
75
|
-
const promises = remotes.map(r =>
|
|
92
|
+
const promises = remotes.map(r => withRetry(
|
|
93
|
+
() => git.pull(projectPath, r, branch),
|
|
94
|
+
3,
|
|
95
|
+
`pull-${r}`
|
|
96
|
+
).then(() => ({ remote: r, ok: true })).catch(e => ({ remote: r, ok: false, error: String(e?.message || e) })));
|
|
97
|
+
|
|
76
98
|
const results = await Promise.all(promises);
|
|
77
99
|
const successful = results.filter(x => x.ok);
|
|
78
100
|
const failed = results.filter(x => !x.ok);
|
|
101
|
+
|
|
102
|
+
let stashMessage = "";
|
|
103
|
+
if (stashed) {
|
|
104
|
+
try {
|
|
105
|
+
console.log("[GitSync] Restoring auto-stash...");
|
|
106
|
+
await git.popStash(projectPath);
|
|
107
|
+
stashMessage = " (mudanças locais restauradas)";
|
|
108
|
+
} catch (popError) {
|
|
109
|
+
stashMessage = " (ATENÇÃO: mudanças salvas no stash, mas houve conflito ao restaurar. Use 'git-stash pop' manualmente)";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
79
113
|
return asToolResult({
|
|
80
114
|
success: successful.length > 0,
|
|
81
115
|
branch,
|
|
82
116
|
pulled: successful.map(x => x.remote),
|
|
83
117
|
failed: failed.map(x => ({ remote: x.remote, error: x.error })),
|
|
84
|
-
message: successful.length === 0 ? "Nenhum remote alcançado.
|
|
118
|
+
message: (successful.length === 0 ? "Nenhum remote alcançado." : `Pull de ${successful.length} remote(s) concluído`) + stashMessage,
|
|
119
|
+
stashed
|
|
85
120
|
});
|
|
86
121
|
}
|
|
87
122
|
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, { availableActions: ["pull", "fetch"] });
|
package/src/tools/git-tags.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
|
|
3
|
-
import { validateProjectPath } from "../utils/repoHelpers.js";
|
|
3
|
+
import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
4
4
|
|
|
5
5
|
const ajv = new Ajv({ allErrors: true });
|
|
6
6
|
|
|
@@ -72,7 +72,7 @@ FLUXO TÍPICO:
|
|
|
72
72
|
if (existing.includes(args.tag)) {
|
|
73
73
|
return asToolError("TAG_ALREADY_EXISTS", `Tag '${args.tag}' já existe`, { tag: args.tag, existingTags: existing });
|
|
74
74
|
}
|
|
75
|
-
await git.createTag(projectPath, args.tag, args.ref || "HEAD", args.message);
|
|
75
|
+
await withRetry(() => git.createTag(projectPath, args.tag, args.ref || "HEAD", args.message), 3, "create-tag");
|
|
76
76
|
return asToolResult({ success: true, tag: args.tag, ref: args.ref || "HEAD", message: `Tag '${args.tag}' criada. Use action='push' para enviar ao GitHub/Gitea.` });
|
|
77
77
|
}
|
|
78
78
|
if (action === "delete") {
|
|
@@ -81,7 +81,7 @@ FLUXO TÍPICO:
|
|
|
81
81
|
if (!existing.includes(args.tag)) {
|
|
82
82
|
return asToolError("TAG_NOT_FOUND", `Tag '${args.tag}' não encontrada`, { tag: args.tag, existingTags: existing });
|
|
83
83
|
}
|
|
84
|
-
await git.deleteTag(projectPath, args.tag);
|
|
84
|
+
await withRetry(() => git.deleteTag(projectPath, args.tag), 3, "delete-tag");
|
|
85
85
|
return asToolResult({ success: true, tag: args.tag, deleted: true });
|
|
86
86
|
}
|
|
87
87
|
if (action === "push") {
|
|
@@ -91,8 +91,8 @@ FLUXO TÍPICO:
|
|
|
91
91
|
return asToolError("TAG_NOT_FOUND", `Tag '${args.tag}' não existe localmente. Crie primeiro com action='create'`, { tag: args.tag, existingTags: existing });
|
|
92
92
|
}
|
|
93
93
|
const results = await Promise.allSettled([
|
|
94
|
-
git.pushTag(projectPath, "github", args.tag),
|
|
95
|
-
git.pushTag(projectPath, "gitea", args.tag)
|
|
94
|
+
withRetry(() => git.pushTag(projectPath, "github", args.tag), 3, "push-tag-github"),
|
|
95
|
+
withRetry(() => git.pushTag(projectPath, "gitea", args.tag), 3, "push-tag-gitea")
|
|
96
96
|
]);
|
|
97
97
|
const pushed = [];
|
|
98
98
|
const failed = [];
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
2
4
|
import { MCPError, asToolError, asToolResult, errorToResponse, createError } from "../utils/errors.js";
|
|
3
|
-
import { getRepoNameFromPath, detectProjectType, GITIGNORE_TEMPLATES, validateProjectPath } from "../utils/repoHelpers.js";
|
|
5
|
+
import { getRepoNameFromPath, detectProjectType, GITIGNORE_TEMPLATES, validateProjectPath, withRetry } from "../utils/repoHelpers.js";
|
|
4
6
|
|
|
5
7
|
const ajv = new Ajv({ allErrors: true });
|
|
6
8
|
|
|
@@ -8,13 +10,13 @@ export function createGitWorkflowTool(pm, git) {
|
|
|
8
10
|
const inputSchema = {
|
|
9
11
|
type: "object",
|
|
10
12
|
properties: {
|
|
11
|
-
projectPath: {
|
|
12
|
-
type: "string",
|
|
13
|
-
description: "Caminho absoluto do diretório do projeto (ex: 'C:/Users/user/projeto' ou '/home/user/projeto')"
|
|
13
|
+
projectPath: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Caminho absoluto do diretório do projeto (ex: 'C:/Users/user/projeto' ou '/home/user/projeto')"
|
|
14
16
|
},
|
|
15
|
-
action: {
|
|
16
|
-
type: "string",
|
|
17
|
-
enum: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean"
|
|
17
|
+
action: {
|
|
18
|
+
type: "string",
|
|
19
|
+
enum: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean"],
|
|
18
20
|
description: `Ação a executar:
|
|
19
21
|
- init: Inicializa repositório git local E cria repos no GitHub/Gitea automaticamente
|
|
20
22
|
- status: Retorna arquivos modificados, staged, untracked (use ANTES de commit para ver o que mudou)
|
|
@@ -23,20 +25,19 @@ export function createGitWorkflowTool(pm, git) {
|
|
|
23
25
|
- commit: Cria commit com os arquivos staged (use DEPOIS de add)
|
|
24
26
|
- push: Envia commits para GitHub E Gitea em paralelo (use DEPOIS de commit)
|
|
25
27
|
- ensure-remotes: Configura remotes GitHub e Gitea (use se push falhar por falta de remote)
|
|
26
|
-
- clean: Remove arquivos não rastreados do working directory
|
|
27
|
-
- update: Atualiza projeto completo: init (se necessário), add, commit, ensure-remotes e push`
|
|
28
|
+
- clean: Remove arquivos não rastreados do working directory`
|
|
28
29
|
},
|
|
29
|
-
files: {
|
|
30
|
-
type: "array",
|
|
30
|
+
files: {
|
|
31
|
+
type: "array",
|
|
31
32
|
items: { type: "string" },
|
|
32
33
|
description: "Lista de arquivos para add/remove. Use ['.'] para todos os arquivos. Ex: ['src/index.js', 'package.json']"
|
|
33
34
|
},
|
|
34
|
-
message: {
|
|
35
|
+
message: {
|
|
35
36
|
type: "string",
|
|
36
|
-
description: "Mensagem do commit. Obrigatório para action='commit'.
|
|
37
|
+
description: "Mensagem do commit. Obrigatório para action='commit'. Ex: 'feat: adiciona nova funcionalidade'"
|
|
37
38
|
},
|
|
38
|
-
force: {
|
|
39
|
-
type: "boolean",
|
|
39
|
+
force: {
|
|
40
|
+
type: "boolean",
|
|
40
41
|
description: "Force push (use apenas se push normal falhar com erro de histórico divergente). Default: false"
|
|
41
42
|
},
|
|
42
43
|
createGitignore: {
|
|
@@ -48,7 +49,7 @@ export function createGitWorkflowTool(pm, git) {
|
|
|
48
49
|
description: "Se true, repositório será PÚBLICO. Default: false (privado). Aplica-se a action='init' e 'ensure-remotes'"
|
|
49
50
|
},
|
|
50
51
|
dryRun: {
|
|
51
|
-
type: "boolean",
|
|
52
|
+
type: "boolean",
|
|
52
53
|
description: "Se true, simula a operação sem executar (útil para testes). Default: false"
|
|
53
54
|
}
|
|
54
55
|
},
|
|
@@ -72,7 +73,6 @@ QUANDO USAR CADA ACTION:
|
|
|
72
73
|
- init: Apenas uma vez, para novos projetos (cria .gitignore automaticamente)
|
|
73
74
|
- ensure-remotes: Se push falhar por falta de configuração
|
|
74
75
|
- clean: Limpar arquivos não rastreados
|
|
75
|
-
- update: Para atualizar projeto completo (init + add + commit + push) - mais rápido que fazer cada ação separadamente
|
|
76
76
|
|
|
77
77
|
EXEMPLOS DE USO:
|
|
78
78
|
• Iniciar projeto: { "projectPath": "/path/to/project", "action": "init" }
|
|
@@ -90,29 +90,29 @@ EXEMPLOS DE USO:
|
|
|
90
90
|
try {
|
|
91
91
|
// #region agent log
|
|
92
92
|
if (process.env.DEBUG_AGENT_LOG) {
|
|
93
|
-
fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H1',location:'git-workflow.js:handle-entry',message:'handle start',data:{action,projectPath},timestamp:Date.now()})}).catch(()=>{});
|
|
93
|
+
fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: 'debug-session', runId: 'run1', hypothesisId: 'H1', location: 'git-workflow.js:handle-entry', message: 'handle start', data: { action, projectPath }, timestamp: Date.now() }) }).catch(() => { });
|
|
94
94
|
}
|
|
95
95
|
// #endregion
|
|
96
96
|
|
|
97
97
|
validateProjectPath(projectPath);
|
|
98
98
|
// #region agent log
|
|
99
99
|
if (process.env.DEBUG_AGENT_LOG) {
|
|
100
|
-
fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H1',location:'git-workflow.js:handle-validated',message:'projectPath validated',data:{action},timestamp:Date.now()})}).catch(()=>{});
|
|
100
|
+
fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: 'debug-session', runId: 'run1', hypothesisId: 'H1', location: 'git-workflow.js:handle-validated', message: 'projectPath validated', data: { action }, timestamp: Date.now() }) }).catch(() => { });
|
|
101
101
|
}
|
|
102
102
|
// #endregion
|
|
103
103
|
if (action === "init") {
|
|
104
104
|
if (args.dryRun) {
|
|
105
|
-
return asToolResult({
|
|
106
|
-
success: true,
|
|
105
|
+
return asToolResult({
|
|
106
|
+
success: true,
|
|
107
107
|
dryRun: true,
|
|
108
108
|
message: "DRY RUN: Repositório seria inicializado localmente e nos providers",
|
|
109
109
|
repoName: getRepoNameFromPath(projectPath),
|
|
110
110
|
gitignoreCreated: shouldCreateGitignore
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
|
-
|
|
113
|
+
|
|
114
114
|
await git.init(projectPath);
|
|
115
|
-
|
|
115
|
+
|
|
116
116
|
// Criar .gitignore baseado no tipo de projeto
|
|
117
117
|
const shouldCreateGitignore = args.createGitignore !== false;
|
|
118
118
|
let gitignoreCreated = false;
|
|
@@ -122,12 +122,12 @@ EXEMPLOS DE USO:
|
|
|
122
122
|
await git.createGitignore(projectPath, patterns);
|
|
123
123
|
gitignoreCreated = true;
|
|
124
124
|
}
|
|
125
|
-
|
|
125
|
+
|
|
126
126
|
const repo = getRepoNameFromPath(projectPath);
|
|
127
127
|
const isPublic = args.isPublic === true; // Default: privado
|
|
128
128
|
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
|
|
129
|
-
return asToolResult({
|
|
130
|
-
success: true,
|
|
129
|
+
return asToolResult({
|
|
130
|
+
success: true,
|
|
131
131
|
ensured,
|
|
132
132
|
isPrivate: !isPublic,
|
|
133
133
|
gitignoreCreated,
|
|
@@ -138,24 +138,24 @@ EXEMPLOS DE USO:
|
|
|
138
138
|
const st = await git.status(projectPath);
|
|
139
139
|
// #region agent log
|
|
140
140
|
if (process.env.DEBUG_AGENT_LOG) {
|
|
141
|
-
fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H2',location:'git-workflow.js:status',message:'status result',data:{modified:st.modified.length,created:st.created.length,deleted:st.deleted.length,notAdded:st.not_added.length,isClean:st.isClean},timestamp:Date.now()})}).catch(()=>{});
|
|
141
|
+
fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: 'debug-session', runId: 'run1', hypothesisId: 'H2', location: 'git-workflow.js:status', message: 'status result', data: { modified: st.modified.length, created: st.created.length, deleted: st.deleted.length, notAdded: st.not_added.length, isClean: st.isClean }, timestamp: Date.now() }) }).catch(() => { });
|
|
142
142
|
}
|
|
143
143
|
// #endregion
|
|
144
|
-
|
|
144
|
+
|
|
145
145
|
if (args.dryRun) {
|
|
146
|
-
return asToolResult({
|
|
147
|
-
success: true,
|
|
146
|
+
return asToolResult({
|
|
147
|
+
success: true,
|
|
148
148
|
dryRun: true,
|
|
149
149
|
message: "DRY RUN: Status seria verificado",
|
|
150
150
|
...st
|
|
151
151
|
});
|
|
152
152
|
}
|
|
153
|
-
|
|
153
|
+
|
|
154
154
|
// Adicionar contexto para AI Agent decidir próximo passo
|
|
155
155
|
const hasUnstaged = st.not_added?.length > 0 || st.modified?.length > 0 || st.created?.length > 0;
|
|
156
156
|
const hasStaged = st.staged?.length > 0;
|
|
157
157
|
const hasConflicts = st.conflicted?.length > 0;
|
|
158
|
-
|
|
158
|
+
|
|
159
159
|
let _aiContext = {
|
|
160
160
|
needsAdd: hasUnstaged && !hasStaged,
|
|
161
161
|
readyToCommit: hasStaged && !hasConflicts,
|
|
@@ -163,7 +163,7 @@ EXEMPLOS DE USO:
|
|
|
163
163
|
isClean: st.isClean,
|
|
164
164
|
suggestedAction: null
|
|
165
165
|
};
|
|
166
|
-
|
|
166
|
+
|
|
167
167
|
if (hasConflicts) {
|
|
168
168
|
_aiContext.suggestedAction = "Resolva os conflitos manualmente antes de continuar";
|
|
169
169
|
} else if (hasStaged) {
|
|
@@ -173,21 +173,21 @@ EXEMPLOS DE USO:
|
|
|
173
173
|
} else {
|
|
174
174
|
_aiContext.suggestedAction = "Working tree limpa. Modifique arquivos ou use action='push' se há commits pendentes";
|
|
175
175
|
}
|
|
176
|
-
|
|
176
|
+
|
|
177
177
|
return asToolResult({ ...st, _aiContext }, { tool: 'workflow', action: 'status' });
|
|
178
178
|
}
|
|
179
179
|
if (action === "add") {
|
|
180
180
|
const files = Array.isArray(args.files) && args.files.length ? args.files : ["."];
|
|
181
|
-
|
|
181
|
+
|
|
182
182
|
if (args.dryRun) {
|
|
183
|
-
return asToolResult({
|
|
184
|
-
success: true,
|
|
183
|
+
return asToolResult({
|
|
184
|
+
success: true,
|
|
185
185
|
dryRun: true,
|
|
186
186
|
message: `DRY RUN: Arquivos seriam adicionados: ${files.join(", ")}`,
|
|
187
187
|
files
|
|
188
188
|
});
|
|
189
189
|
}
|
|
190
|
-
|
|
190
|
+
|
|
191
191
|
await git.add(projectPath, files);
|
|
192
192
|
return asToolResult({ success: true, files }, { tool: 'workflow', action: 'add' });
|
|
193
193
|
}
|
|
@@ -200,19 +200,19 @@ EXEMPLOS DE USO:
|
|
|
200
200
|
if (!args.message) {
|
|
201
201
|
return asToolError("MISSING_PARAMETER", "message é obrigatório para commit", { parameter: "message" });
|
|
202
202
|
}
|
|
203
|
-
|
|
203
|
+
|
|
204
204
|
if (args.dryRun) {
|
|
205
|
-
return asToolResult({
|
|
206
|
-
success: true,
|
|
205
|
+
return asToolResult({
|
|
206
|
+
success: true,
|
|
207
207
|
dryRun: true,
|
|
208
208
|
message: `DRY RUN: Commit seria criado com mensagem: "${args.message}"`
|
|
209
209
|
});
|
|
210
210
|
}
|
|
211
|
-
|
|
211
|
+
|
|
212
212
|
const sha = await git.commit(projectPath, args.message);
|
|
213
213
|
// #region agent log
|
|
214
214
|
if (process.env.DEBUG_AGENT_LOG) {
|
|
215
|
-
fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({sessionId:'debug-session',runId:'run1',hypothesisId:'H3',location:'git-workflow.js:commit',message:'commit created',data:{sha,message:args.message},timestamp:Date.now()})}).catch(()=>{});
|
|
215
|
+
fetch('http://127.0.0.1:8243/ingest/a7114eec-653b-43b0-9f09-7073baee17bf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: 'debug-session', runId: 'run1', hypothesisId: 'H3', location: 'git-workflow.js:commit', message: 'commit created', data: { sha, message: args.message }, timestamp: Date.now() }) }).catch(() => { });
|
|
216
216
|
}
|
|
217
217
|
// #endregion
|
|
218
218
|
return asToolResult({ success: true, sha, message: args.message }, { tool: 'workflow', action: 'commit' });
|
|
@@ -220,19 +220,19 @@ EXEMPLOS DE USO:
|
|
|
220
220
|
if (action === "clean") {
|
|
221
221
|
if (args.dryRun) {
|
|
222
222
|
const result = await git.cleanUntracked(projectPath);
|
|
223
|
-
return asToolResult({
|
|
224
|
-
success: true,
|
|
223
|
+
return asToolResult({
|
|
224
|
+
success: true,
|
|
225
225
|
dryRun: true,
|
|
226
226
|
message: `DRY RUN: ${result.cleaned.length} arquivo(s) seriam removidos`,
|
|
227
227
|
wouldClean: result.cleaned
|
|
228
228
|
});
|
|
229
229
|
}
|
|
230
|
-
|
|
230
|
+
|
|
231
231
|
const result = await git.cleanUntracked(projectPath);
|
|
232
|
-
return asToolResult({
|
|
233
|
-
success: true,
|
|
232
|
+
return asToolResult({
|
|
233
|
+
success: true,
|
|
234
234
|
...result,
|
|
235
|
-
message: result.cleaned.length > 0
|
|
235
|
+
message: result.cleaned.length > 0
|
|
236
236
|
? `${result.cleaned.length} arquivo(s) não rastreados removidos`
|
|
237
237
|
: "Nenhum arquivo para limpar"
|
|
238
238
|
});
|
|
@@ -240,7 +240,7 @@ EXEMPLOS DE USO:
|
|
|
240
240
|
if (action === "ensure-remotes") {
|
|
241
241
|
const repo = getRepoNameFromPath(projectPath);
|
|
242
242
|
const isPublic = args.isPublic === true; // Default: privado
|
|
243
|
-
|
|
243
|
+
|
|
244
244
|
if (args.dryRun) {
|
|
245
245
|
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: false, isPublic }); // Don't create for dry run
|
|
246
246
|
const ghOwner = await pm.getGitHubOwner();
|
|
@@ -248,9 +248,9 @@ EXEMPLOS DE USO:
|
|
|
248
248
|
const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
|
|
249
249
|
const base = pm.giteaUrl?.replace(/\/$/, "") || "";
|
|
250
250
|
const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
|
|
251
|
-
|
|
252
|
-
return asToolResult({
|
|
253
|
-
success: true,
|
|
251
|
+
|
|
252
|
+
return asToolResult({
|
|
253
|
+
success: true,
|
|
254
254
|
dryRun: true,
|
|
255
255
|
message: "DRY RUN: Remotes seriam configurados",
|
|
256
256
|
repo,
|
|
@@ -259,7 +259,7 @@ EXEMPLOS DE USO:
|
|
|
259
259
|
ensured
|
|
260
260
|
});
|
|
261
261
|
}
|
|
262
|
-
|
|
262
|
+
|
|
263
263
|
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
|
|
264
264
|
const ghOwner = await pm.getGitHubOwner();
|
|
265
265
|
const geOwner = await pm.getGiteaOwner();
|
|
@@ -273,120 +273,28 @@ EXEMPLOS DE USO:
|
|
|
273
273
|
if (action === "push") {
|
|
274
274
|
const branch = await git.getCurrentBranch(projectPath);
|
|
275
275
|
const force = !!args.force;
|
|
276
|
-
|
|
276
|
+
|
|
277
277
|
if (args.dryRun) {
|
|
278
|
-
return asToolResult({
|
|
279
|
-
success: true,
|
|
278
|
+
return asToolResult({
|
|
279
|
+
success: true,
|
|
280
280
|
dryRun: true,
|
|
281
281
|
message: `DRY RUN: Push seria executado na branch '${branch}'${force ? ' (force)' : ''}`,
|
|
282
282
|
branch,
|
|
283
283
|
force
|
|
284
284
|
});
|
|
285
285
|
}
|
|
286
|
+
|
|
287
|
+
// Retry logic for push (often fails due to network or concurrent updates)
|
|
288
|
+
const result = await withRetry(
|
|
289
|
+
() => git.pushParallel(projectPath, branch, force),
|
|
290
|
+
3,
|
|
291
|
+
"push"
|
|
292
|
+
);
|
|
286
293
|
|
|
287
|
-
const result = await git.pushParallel(projectPath, branch, force);
|
|
288
294
|
return asToolResult({ success: true, branch, ...result });
|
|
289
295
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return asToolResult({
|
|
293
|
-
success: true,
|
|
294
|
-
dryRun: true,
|
|
295
|
-
message: "DRY RUN: Update completo seria executado (init se necessário, add, commit, push)"
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const results = {
|
|
300
|
-
init: null,
|
|
301
|
-
ensureRemotes: null,
|
|
302
|
-
add: null,
|
|
303
|
-
commit: null,
|
|
304
|
-
push: null
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
const repo = getRepoNameFromPath(projectPath);
|
|
308
|
-
const isPublic = args.isPublic === true;
|
|
309
|
-
|
|
310
|
-
// 1. Verificar se é repo Git, se não for, fazer init
|
|
311
|
-
const isRepo = await git.isRepo(projectPath).catch(() => false);
|
|
312
|
-
if (!isRepo) {
|
|
313
|
-
await git.init(projectPath);
|
|
314
|
-
|
|
315
|
-
// Criar .gitignore baseado no tipo de projeto
|
|
316
|
-
const shouldCreateGitignore = args.createGitignore !== false;
|
|
317
|
-
if (shouldCreateGitignore) {
|
|
318
|
-
const projectType = detectProjectType(projectPath);
|
|
319
|
-
const patterns = GITIGNORE_TEMPLATES[projectType] || GITIGNORE_TEMPLATES.general;
|
|
320
|
-
await git.createGitignore(projectPath, patterns);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
|
|
324
|
-
results.init = { success: true, ensured, isPrivate: !isPublic, gitignoreCreated: shouldCreateGitignore };
|
|
325
|
-
} else {
|
|
326
|
-
results.init = { success: true, skipped: true, message: "Repositório já existe" };
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// 2. Garantir remotes configurados (sempre verifica/configura)
|
|
330
|
-
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
|
|
331
|
-
const ghOwner = await pm.getGitHubOwner();
|
|
332
|
-
const geOwner = await pm.getGiteaOwner();
|
|
333
|
-
const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
|
|
334
|
-
const base = pm.giteaUrl?.replace(/\/$/, "") || "";
|
|
335
|
-
const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
|
|
336
|
-
await git.ensureRemotes(projectPath, { githubUrl, giteaUrl });
|
|
337
|
-
const remotes = await git.listRemotes(projectPath);
|
|
338
|
-
results.ensureRemotes = { success: true, ensured, remotes };
|
|
339
|
-
|
|
340
|
-
// 3. Verificar status e fazer add se necessário
|
|
341
|
-
const status = await git.status(projectPath);
|
|
342
|
-
if (!status.isClean || status.modified?.length > 0 || status.created?.length > 0 || status.notAdded?.length > 0) {
|
|
343
|
-
await git.add(projectPath, ["."]);
|
|
344
|
-
results.add = { success: true, files: ["."] };
|
|
345
|
-
} else {
|
|
346
|
-
results.add = { success: true, skipped: true, message: "Nenhum arquivo para adicionar" };
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// 4. Verificar se há algo staged e fazer commit
|
|
350
|
-
const statusAfterAdd = await git.status(projectPath);
|
|
351
|
-
if (statusAfterAdd.staged?.length > 0) {
|
|
352
|
-
// Gerar mensagem padrão se não fornecida
|
|
353
|
-
const commitMessage = args.message || `Update: ${new Date().toISOString().split('T')[0]} - ${statusAfterAdd.staged.length} arquivo(s) modificado(s)`;
|
|
354
|
-
const sha = await git.commit(projectPath, commitMessage);
|
|
355
|
-
results.commit = { success: true, sha, message: commitMessage };
|
|
356
|
-
} else {
|
|
357
|
-
results.commit = { success: true, skipped: true, message: "Nenhum arquivo staged para commit" };
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// 5. Fazer push (só se houver commits para enviar)
|
|
361
|
-
const branch = await git.getCurrentBranch(projectPath);
|
|
362
|
-
const force = !!args.force;
|
|
363
|
-
try {
|
|
364
|
-
const pushResult = await git.pushParallel(projectPath, branch, force);
|
|
365
|
-
results.push = { success: true, branch, ...pushResult };
|
|
366
|
-
} catch (pushError) {
|
|
367
|
-
// Se push falhar mas não houver commits, não é erro crítico
|
|
368
|
-
if (results.commit?.skipped) {
|
|
369
|
-
results.push = { success: true, skipped: true, message: "Nenhum commit para enviar" };
|
|
370
|
-
} else {
|
|
371
|
-
throw pushError;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Resumo final
|
|
376
|
-
const allSuccess = Object.values(results).every(r => r?.success !== false);
|
|
377
|
-
const stepsExecuted = Object.entries(results)
|
|
378
|
-
.filter(([_, r]) => r && !r.skipped)
|
|
379
|
-
.map(([step, _]) => step);
|
|
380
|
-
|
|
381
|
-
return asToolResult({
|
|
382
|
-
success: allSuccess,
|
|
383
|
-
message: `Update completo executado: ${stepsExecuted.join(" → ")}`,
|
|
384
|
-
results,
|
|
385
|
-
stepsExecuted
|
|
386
|
-
}, { tool: 'workflow', action: 'update' });
|
|
387
|
-
}
|
|
388
|
-
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
|
|
389
|
-
availableActions: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean", "update"],
|
|
296
|
+
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
|
|
297
|
+
availableActions: ["init", "status", "add", "remove", "commit", "push", "ensure-remotes", "clean"],
|
|
390
298
|
suggestion: "Use uma das actions disponíveis"
|
|
391
299
|
});
|
|
392
300
|
} catch (e) {
|
package/src/utils/gitAdapter.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { MCPError, createError, mapExternalError } from "./errors.js";
|
|
5
|
-
import { getRepoNameFromPath, getProvidersEnv } from "./repoHelpers.js";
|
|
5
|
+
import { getRepoNameFromPath, getProvidersEnv, findGitRoot } from "./repoHelpers.js";
|
|
6
6
|
|
|
7
7
|
// Common locations for Git on Windows, Linux, and macOS
|
|
8
8
|
const GIT_CANDIDATES = [
|
package/src/utils/repoHelpers.js
CHANGED
|
@@ -66,6 +66,26 @@ export function validateFilePath(projectPath, filePath) {
|
|
|
66
66
|
return absoluteFile;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Encontra a raiz do repositório Git subindo a árvore de diretórios
|
|
71
|
+
* @param {string} startDir - Diretório inicial
|
|
72
|
+
* @returns {string|null} Caminho da raiz do repo ou null se não encontrado
|
|
73
|
+
*/
|
|
74
|
+
export function findGitRoot(startDir) {
|
|
75
|
+
let current = path.resolve(startDir);
|
|
76
|
+
const root = path.parse(current).root;
|
|
77
|
+
|
|
78
|
+
while (true) {
|
|
79
|
+
if (fs.existsSync(path.join(current, ".git"))) {
|
|
80
|
+
return current;
|
|
81
|
+
}
|
|
82
|
+
if (current === root) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
current = path.dirname(current);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
69
89
|
export function getEnv(key) {
|
|
70
90
|
const v = process.env[key];
|
|
71
91
|
return v === undefined ? "" : String(v);
|
|
@@ -158,3 +178,39 @@ export function detectProjectType(projectPath) {
|
|
|
158
178
|
return "general";
|
|
159
179
|
}
|
|
160
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Executa uma operação com retries exponenciais
|
|
183
|
+
* @param {Function} operation - Função assíncrona a executar
|
|
184
|
+
* @param {number} maxRetries - Número máximo de tentativas
|
|
185
|
+
* @param {string} context - Contexto para logs
|
|
186
|
+
* @returns {Promise<any>} Resultado da operação
|
|
187
|
+
*/
|
|
188
|
+
export async function withRetry(operation, maxRetries = 3, context = "") {
|
|
189
|
+
let lastError;
|
|
190
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
191
|
+
try {
|
|
192
|
+
return await operation();
|
|
193
|
+
} catch (e) {
|
|
194
|
+
lastError = e;
|
|
195
|
+
const msg = e.message || String(e);
|
|
196
|
+
// Retry on network errors, lock files, or timeouts
|
|
197
|
+
const isRetryable = msg.includes("lock") ||
|
|
198
|
+
msg.includes("network") ||
|
|
199
|
+
msg.includes("resolve host") ||
|
|
200
|
+
msg.includes("timeout") ||
|
|
201
|
+
msg.includes("connection") ||
|
|
202
|
+
msg.includes("ECONNRESET") ||
|
|
203
|
+
msg.includes("ETIMEDOUT");
|
|
204
|
+
|
|
205
|
+
if (!isRetryable && i === 0) throw e; // Fail fast if not retryable and first attempt
|
|
206
|
+
|
|
207
|
+
if (i < maxRetries - 1) {
|
|
208
|
+
const delay = 2000 * Math.pow(2, i);
|
|
209
|
+
console.warn(`[${context}] Attempt ${i + 1} failed, retrying in ${delay}ms... Error: ${msg}`);
|
|
210
|
+
await new Promise(r => setTimeout(r, delay));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
throw lastError;
|
|
215
|
+
}
|
|
216
|
+
|