@andrebuzeli/git-mcp 11.0.5 → 12.0.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/README.md +39 -47
- package/package.json +24 -19
- package/src/index.js +84 -0
- package/src/providers/providerManager.js +107 -0
- package/src/tools/git-branches.js +63 -0
- package/src/tools/git-config.js +53 -0
- package/src/tools/git-files.js +41 -0
- package/src/tools/git-history.js +36 -0
- package/src/tools/git-ignore.js +48 -0
- package/src/tools/git-issues.js +58 -12
- package/src/tools/git-pulls.js +61 -0
- package/src/tools/git-remote.js +182 -29
- package/src/tools/git-reset.js +35 -0
- package/src/tools/git-stash.js +57 -0
- package/src/tools/git-sync.js +44 -40
- package/src/tools/git-tags.js +58 -0
- package/src/tools/git-workflow.js +85 -0
- package/src/utils/errors.js +22 -0
- package/src/utils/gitAdapter.js +116 -0
- package/src/utils/providerExec.js +13 -0
- package/src/utils/repoHelpers.js +25 -0
- package/src/utils/retry.js +12 -0
- package/bin/git-mcp.js +0 -21
- package/docs/TOOLS.md +0 -110
- package/mcp.json.template +0 -12
- package/src/local/git.js +0 -14
- package/src/providers/gitea.js +0 -13
- package/src/providers/github.js +0 -13
- package/src/server.js +0 -130
- package/src/tools/git-actions.js +0 -19
- package/src/tools/git-activity.js +0 -28
- package/src/tools/git-admin.js +0 -20
- package/src/tools/git-checks.js +0 -14
- package/src/tools/git-commits.js +0 -34
- package/src/tools/git-contents.js +0 -30
- package/src/tools/git-deployments.js +0 -21
- package/src/tools/git-gists.js +0 -15
- package/src/tools/git-gitdata.js +0 -19
- package/src/tools/git-issues-prs.js +0 -44
- package/src/tools/git-local.js +0 -66
- package/src/tools/git-meta.js +0 -19
- package/src/tools/git-misc.js +0 -21
- package/src/tools/git-orgs.js +0 -26
- package/src/tools/git-packages.js +0 -12
- package/src/tools/git-raw.js +0 -14
- package/src/tools/git-releases.js +0 -17
- package/src/tools/git-repos.js +0 -60
- package/src/tools/git-search.js +0 -18
- package/src/tools/git-user.js +0 -26
- package/src/tools/schema.js +0 -3
- package/src/utils/fs.js +0 -29
- package/src/utils/project.js +0 -7
- package/tests/errors.js +0 -26
- package/tests/full_suite.js +0 -98
- package/tests/run.js +0 -50
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
4
|
+
import { getRepoNameFromPath } from "../utils/repoHelpers.js";
|
|
5
|
+
import { runBoth } from "../utils/providerExec.js";
|
|
6
|
+
|
|
7
|
+
const ajv = new Ajv({ allErrors: true });
|
|
8
|
+
|
|
9
|
+
export function createGitPullsTool(pm) {
|
|
10
|
+
const inputSchema = {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
projectPath: { type: "string" },
|
|
14
|
+
action: { type: "string", enum: ["create", "list", "files"] },
|
|
15
|
+
title: { type: "string" },
|
|
16
|
+
head: { type: "string" },
|
|
17
|
+
base: { type: "string" },
|
|
18
|
+
number: { type: "number" }
|
|
19
|
+
},
|
|
20
|
+
required: ["projectPath", "action"],
|
|
21
|
+
additionalProperties: true
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
async function handle(args) {
|
|
25
|
+
const validate = ajv.compile(inputSchema);
|
|
26
|
+
if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
27
|
+
const repo = getRepoNameFromPath(args.projectPath);
|
|
28
|
+
try {
|
|
29
|
+
if (args.action === "create") {
|
|
30
|
+
const head = args.head || "feature";
|
|
31
|
+
const base = args.base || "main";
|
|
32
|
+
const out = await runBoth(pm, {
|
|
33
|
+
github: async (owner) => { const r = await pm.github.rest.pulls.create({ owner, repo, title: args.title || `${head} -> ${base}`, head, base }); return { ok: true, number: r.data.number }; },
|
|
34
|
+
gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls`, { title: args.title || `${head} -> ${base}`, head, base }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, number: r.data?.number }; }
|
|
35
|
+
});
|
|
36
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
|
|
37
|
+
}
|
|
38
|
+
if (args.action === "list") {
|
|
39
|
+
const out = await runBoth(pm, {
|
|
40
|
+
github: async (owner) => { const r = await pm.github.rest.pulls.list({ owner, repo, state: "all" }); return { ok: true, count: r.data.length }; },
|
|
41
|
+
gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data||[]).length }; }
|
|
42
|
+
});
|
|
43
|
+
return asToolResult({ providers: out });
|
|
44
|
+
}
|
|
45
|
+
if (args.action === "files") {
|
|
46
|
+
const num = args.number;
|
|
47
|
+
if (!num) return asToolError("VALIDATION_ERROR", "number é obrigatório");
|
|
48
|
+
const out = await runBoth(pm, {
|
|
49
|
+
github: async (owner) => { const r = await pm.github.rest.pulls.listFiles({ owner, repo, pull_number: num }); return { ok: true, count: r.data.length }; },
|
|
50
|
+
gitea: async (owner) => { const baseUrl = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${baseUrl}/api/v1/repos/${owner}/${repo}/pulls/${num}/files`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data||[]).length }; }
|
|
51
|
+
});
|
|
52
|
+
return asToolResult({ providers: out });
|
|
53
|
+
}
|
|
54
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${args.action}`);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return asToolError(e.code || "ERROR", e.message || String(e));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return { name: "git-pulls", description: "Pull Requests em paralelo (GitHub + Gitea)", inputSchema, handle };
|
|
61
|
+
}
|
package/src/tools/git-remote.js
CHANGED
|
@@ -1,29 +1,182 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
4
|
+
import { getRepoNameFromPath } from "../utils/repoHelpers.js";
|
|
5
|
+
import { runBoth } from "../utils/providerExec.js";
|
|
6
|
+
|
|
7
|
+
const ajv = new Ajv({ allErrors: true });
|
|
8
|
+
|
|
9
|
+
export function createGitRemoteTool(pm, git) {
|
|
10
|
+
const inputSchema = {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: {
|
|
13
|
+
projectPath: { type: "string" },
|
|
14
|
+
action: { type: "string", enum: [
|
|
15
|
+
"list",
|
|
16
|
+
"ensure",
|
|
17
|
+
"set-url",
|
|
18
|
+
"repo-delete",
|
|
19
|
+
"release-create",
|
|
20
|
+
"topics-set",
|
|
21
|
+
"milestone-create",
|
|
22
|
+
"label-create",
|
|
23
|
+
"fork-create",
|
|
24
|
+
"fork-list",
|
|
25
|
+
"star-set",
|
|
26
|
+
"star-unset",
|
|
27
|
+
"star-check",
|
|
28
|
+
"subscription-set",
|
|
29
|
+
"subscription-unset",
|
|
30
|
+
"contents-create"
|
|
31
|
+
] },
|
|
32
|
+
tag: { type: "string" },
|
|
33
|
+
name: { type: "string" },
|
|
34
|
+
body: { type: "string" },
|
|
35
|
+
topics: { type: "array", items: { type: "string" } },
|
|
36
|
+
path: { type: "string" },
|
|
37
|
+
content: { type: "string" },
|
|
38
|
+
branch: { type: "string" }
|
|
39
|
+
},
|
|
40
|
+
required: ["projectPath", "action"],
|
|
41
|
+
additionalProperties: true
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
async function handle(args) {
|
|
45
|
+
const validate = ajv.compile(inputSchema);
|
|
46
|
+
if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
47
|
+
const { projectPath, action } = args;
|
|
48
|
+
try {
|
|
49
|
+
if (action === "list") {
|
|
50
|
+
const remotes = await git.listRemotes(projectPath);
|
|
51
|
+
return asToolResult({ remotes });
|
|
52
|
+
}
|
|
53
|
+
if (action === "ensure") {
|
|
54
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
55
|
+
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true });
|
|
56
|
+
const ghOwner = await pm.getGitHubOwner();
|
|
57
|
+
const geOwner = await pm.getGiteaOwner();
|
|
58
|
+
const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
|
|
59
|
+
const base = pm.giteaUrl?.replace(/\/$/, "") || "";
|
|
60
|
+
const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
|
|
61
|
+
await git.ensureRemotes(projectPath, { githubUrl, giteaUrl });
|
|
62
|
+
const remotes = await git.listRemotes(projectPath);
|
|
63
|
+
return asToolResult({ success: true, ensured, remotes });
|
|
64
|
+
}
|
|
65
|
+
if (action === "set-url") {
|
|
66
|
+
// handled via ensure (auto)
|
|
67
|
+
return asToolResult({ success: true, message: "Use action=ensure para set-url automático" });
|
|
68
|
+
}
|
|
69
|
+
if (action === "repo-delete") {
|
|
70
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
71
|
+
const out = await runBoth(pm, {
|
|
72
|
+
github: async (owner) => { await pm.github.rest.repos.delete({ owner, repo }); return { ok: true }; },
|
|
73
|
+
gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); await axios.delete(`${base}/api/v1/repos/${owner}/${repo}`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
|
|
74
|
+
});
|
|
75
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
|
|
76
|
+
}
|
|
77
|
+
if (action === "release-create") {
|
|
78
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
79
|
+
const tag = args.tag || "v1.0.0";
|
|
80
|
+
const name = args.name || tag;
|
|
81
|
+
const body = args.body || "";
|
|
82
|
+
const out = await runBoth(pm, {
|
|
83
|
+
github: async (owner) => { const r = await pm.github.rest.repos.createRelease({ owner, repo, tag_name: tag, name, body, draft: false, prerelease: false }); return { ok: true, id: r.data.id }; },
|
|
84
|
+
gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${base}/api/v1/repos/${owner}/${repo}/releases`, { tag_name: tag, name, body }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, id: r.data?.id }; }
|
|
85
|
+
});
|
|
86
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
|
|
87
|
+
}
|
|
88
|
+
if (action === "topics-set") {
|
|
89
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
90
|
+
const topics = Array.isArray(args.topics) ? args.topics : [];
|
|
91
|
+
const out = await runBoth(pm, {
|
|
92
|
+
github: async (owner) => { await pm.github.request("PUT /repos/{owner}/{repo}/topics", { owner, repo, names: topics, headers: { accept: "application/vnd.github.mercy-preview+json" } }); return { ok: true }; },
|
|
93
|
+
gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); await axios.put(`${base}/api/v1/repos/${owner}/${repo}/topics`, { topics }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
|
|
94
|
+
});
|
|
95
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
|
|
96
|
+
}
|
|
97
|
+
if (action === "milestone-create") {
|
|
98
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
99
|
+
const title = args.title || "v1.0";
|
|
100
|
+
const out = await runBoth(pm, {
|
|
101
|
+
github: async (owner) => { const r = await pm.github.request("POST /repos/{owner}/{repo}/milestones", { owner, repo, title }); return { ok: true, id: r.data?.id }; },
|
|
102
|
+
gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${base}/api/v1/repos/${owner}/${repo}/milestones`, { title }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, id: r.data?.id }; }
|
|
103
|
+
});
|
|
104
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
|
|
105
|
+
}
|
|
106
|
+
if (action === "label-create") {
|
|
107
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
108
|
+
const name = args.name || "bug";
|
|
109
|
+
const color = String(args.color || "ff0000").replace(/^#/, "");
|
|
110
|
+
const out = await runBoth(pm, {
|
|
111
|
+
github: async (owner) => { await pm.github.request("POST /repos/{owner}/{repo}/labels", { owner, repo, name, color }); return { ok: true }; },
|
|
112
|
+
gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); await axios.post(`${base}/api/v1/repos/${owner}/${repo}/labels`, { name, color }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
|
|
113
|
+
});
|
|
114
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
|
|
115
|
+
}
|
|
116
|
+
if (action === "fork-create") {
|
|
117
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
118
|
+
const out = await runBoth(pm, {
|
|
119
|
+
github: async (owner) => { const r = await pm.github.rest.repos.createFork({ owner, repo }); return { ok: true, full_name: r.data?.full_name }; },
|
|
120
|
+
gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.post(`${base}/api/v1/repos/${owner}/${repo}/forks`, {}, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, full_name: r.data?.full_name }; }
|
|
121
|
+
});
|
|
122
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
|
|
123
|
+
}
|
|
124
|
+
if (action === "fork-list") {
|
|
125
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
126
|
+
const out = await runBoth(pm, {
|
|
127
|
+
github: async (owner) => { const r = await pm.github.rest.repos.listForks({ owner, repo }); return { ok: true, count: r.data.length }; },
|
|
128
|
+
gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); const r = await axios.get(`${base}/api/v1/repos/${owner}/${repo}/forks`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true, count: (r.data || []).length }; }
|
|
129
|
+
});
|
|
130
|
+
return asToolResult({ providers: out });
|
|
131
|
+
}
|
|
132
|
+
if (action === "star-set" || action === "star-unset" || action === "star-check") {
|
|
133
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
134
|
+
const out = await runBoth(pm, {
|
|
135
|
+
github: async (owner) => {
|
|
136
|
+
if (action === "star-set") { await pm.github.request("PUT /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true }; }
|
|
137
|
+
if (action === "star-unset") { await pm.github.request("DELETE /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true }; }
|
|
138
|
+
const r = await pm.github.request("GET /user/starred/{owner}/{repo}", { owner, repo }); return { ok: r.status === 204 || r.status === 200 };
|
|
139
|
+
},
|
|
140
|
+
gitea: async (owner) => {
|
|
141
|
+
const base = pm.giteaUrl.replace(/\/$/, "");
|
|
142
|
+
if (action === "star-set") { await axios.put(`${base}/api/v1/user/starred/${owner}/${repo}`, {}, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
|
|
143
|
+
if (action === "star-unset") { await axios.delete(`${base}/api/v1/user/starred/${owner}/${repo}`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
|
|
144
|
+
const r = await axios.get(`${base}/api/v1/user/starred/${owner}/${repo}`, { headers: { Authorization: `token ${pm.giteaToken}` }, validateStatus: () => true }); return { ok: r.status === 204 || r.status === 200 };
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
|
|
148
|
+
}
|
|
149
|
+
if (action === "subscription-set" || action === "subscription-unset") {
|
|
150
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
151
|
+
const out = await runBoth(pm, {
|
|
152
|
+
github: async (owner) => { if (action === "subscription-set") { await pm.github.request("PUT /repos/{owner}/{repo}/subscription", { owner, repo, subscribed: true }); } else { await pm.github.request("DELETE /repos/{owner}/{repo}/subscription", { owner, repo }); } return { ok: true }; },
|
|
153
|
+
gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); if (action === "subscription-set") { await axios.put(`${base}/api/v1/repos/${owner}/${repo}/subscription`, { subscribed: true }, { headers: { Authorization: `token ${pm.giteaToken}` } }); } else { await axios.delete(`${base}/api/v1/repos/${owner}/${repo}/subscription`, { headers: { Authorization: `token ${pm.giteaToken}` } }); } return { ok: true }; }
|
|
154
|
+
});
|
|
155
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
|
|
156
|
+
}
|
|
157
|
+
if (action === "contents-create") {
|
|
158
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
159
|
+
const filePath = args.path || "test.txt";
|
|
160
|
+
const message = args.message || "Add file";
|
|
161
|
+
const data = typeof args.content === "string" ? args.content : "";
|
|
162
|
+
const branch = args.branch || "main";
|
|
163
|
+
const b64 = Buffer.from(data, "utf8").toString("base64");
|
|
164
|
+
const out = await runBoth(pm, {
|
|
165
|
+
github: async (owner) => { await pm.github.request("PUT /repos/{owner}/{repo}/contents/{path}", { owner, repo, path: filePath, message, content: b64, branch }); return { ok: true }; },
|
|
166
|
+
gitea: async (owner) => { const base = pm.giteaUrl.replace(/\/$/, ""); await axios.post(`${base}/api/v1/repos/${owner}/${repo}/contents/${encodeURIComponent(filePath)}`, { content: b64, message, branch }, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
|
|
167
|
+
});
|
|
168
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), path: filePath, providers: out });
|
|
169
|
+
}
|
|
170
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
|
|
171
|
+
} catch (e) {
|
|
172
|
+
return asToolError(e.code || "ERROR", e.message || String(e));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
name: "git-remote",
|
|
178
|
+
description: "Gerencia remotes e garante GitHub + Gitea",
|
|
179
|
+
inputSchema,
|
|
180
|
+
handle
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
3
|
+
|
|
4
|
+
const ajv = new Ajv({ allErrors: true });
|
|
5
|
+
|
|
6
|
+
export function createGitResetTool(git) {
|
|
7
|
+
const inputSchema = {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
projectPath: { type: "string" },
|
|
11
|
+
action: { type: "string", enum: ["soft", "mixed", "hard"] },
|
|
12
|
+
ref: { type: "string" }
|
|
13
|
+
},
|
|
14
|
+
required: ["projectPath", "action", "ref"],
|
|
15
|
+
additionalProperties: true
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
async function handle(args) {
|
|
19
|
+
const validate = ajv.compile(inputSchema);
|
|
20
|
+
if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
21
|
+
const { projectPath, action, ref } = args;
|
|
22
|
+
try {
|
|
23
|
+
if (action === "soft") await git.resetSoft(projectPath, ref);
|
|
24
|
+
else if (action === "mixed") await git.resetMixed(projectPath, ref);
|
|
25
|
+
else if (action === "hard") await git.resetHard(projectPath, ref);
|
|
26
|
+
else return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
|
|
27
|
+
return asToolResult({ success: true, action, ref });
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return asToolError(e.code || "ERROR", e.message || String(e));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { name: "git-reset", description: "Reset soft/mixed/hard", inputSchema, handle };
|
|
34
|
+
}
|
|
35
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
3
|
+
|
|
4
|
+
const ajv = new Ajv({ allErrors: true });
|
|
5
|
+
|
|
6
|
+
export function createGitStashTool(git) {
|
|
7
|
+
const inputSchema = {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
projectPath: { type: "string" },
|
|
11
|
+
action: { type: "string", enum: ["list", "save", "apply", "pop", "drop", "clear"] },
|
|
12
|
+
message: { type: "string" },
|
|
13
|
+
ref: { type: "string" },
|
|
14
|
+
includeUntracked: { type: "boolean" }
|
|
15
|
+
},
|
|
16
|
+
required: ["projectPath", "action"],
|
|
17
|
+
additionalProperties: true
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async function handle(args) {
|
|
21
|
+
const validate = ajv.compile(inputSchema);
|
|
22
|
+
if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
23
|
+
const { projectPath, action } = args;
|
|
24
|
+
try {
|
|
25
|
+
if (action === "list") {
|
|
26
|
+
const items = await git.listStash(projectPath);
|
|
27
|
+
return asToolResult({ items });
|
|
28
|
+
}
|
|
29
|
+
if (action === "save") {
|
|
30
|
+
await git.saveStash(projectPath, args.message || "WIP", !!args.includeUntracked);
|
|
31
|
+
return asToolResult({ success: true });
|
|
32
|
+
}
|
|
33
|
+
if (action === "apply") {
|
|
34
|
+
await git.applyStash(projectPath, args.ref);
|
|
35
|
+
return asToolResult({ success: true });
|
|
36
|
+
}
|
|
37
|
+
if (action === "pop") {
|
|
38
|
+
await git.popStash(projectPath, args.ref);
|
|
39
|
+
return asToolResult({ success: true });
|
|
40
|
+
}
|
|
41
|
+
if (action === "drop") {
|
|
42
|
+
await git.dropStash(projectPath, args.ref);
|
|
43
|
+
return asToolResult({ success: true });
|
|
44
|
+
}
|
|
45
|
+
if (action === "clear") {
|
|
46
|
+
await git.clearStash(projectPath);
|
|
47
|
+
return asToolResult({ success: true });
|
|
48
|
+
}
|
|
49
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return asToolError(e.code || "ERROR", e.message || String(e));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { name: "git-stash", description: "Gerencia stash", inputSchema, handle };
|
|
56
|
+
}
|
|
57
|
+
|
package/src/tools/git-sync.js
CHANGED
|
@@ -1,40 +1,44 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
3
|
+
|
|
4
|
+
const ajv = new Ajv({ allErrors: true });
|
|
5
|
+
|
|
6
|
+
export function createGitSyncTool(git) {
|
|
7
|
+
const inputSchema = {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
projectPath: { type: "string" },
|
|
11
|
+
action: { type: "string", enum: ["pull", "fetch"] },
|
|
12
|
+
remote: { type: "string" },
|
|
13
|
+
branch: { type: "string" }
|
|
14
|
+
},
|
|
15
|
+
required: ["projectPath", "action"],
|
|
16
|
+
additionalProperties: true
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
async function handle(args) {
|
|
20
|
+
const validate = ajv.compile(inputSchema);
|
|
21
|
+
if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
22
|
+
const { projectPath, action } = args;
|
|
23
|
+
try {
|
|
24
|
+
const branch = args.branch || await git.getCurrentBranch(projectPath);
|
|
25
|
+
if (action === "fetch") {
|
|
26
|
+
const remotes = args.remote ? [args.remote] : ["origin", "github", "gitea"];
|
|
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
|
+
const results = await Promise.all(promises);
|
|
29
|
+
return asToolResult({ success: results.some(x => x.ok), branch, results });
|
|
30
|
+
}
|
|
31
|
+
if (action === "pull") {
|
|
32
|
+
const remotes = args.remote ? [args.remote] : ["origin", "github", "gitea"];
|
|
33
|
+
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
|
+
const results = await Promise.all(promises);
|
|
35
|
+
return asToolResult({ success: results.some(x => x.ok), branch, results });
|
|
36
|
+
}
|
|
37
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
return asToolError(e.code || "ERROR", e.message || String(e));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { name: "git-sync", description: "fetch/pull (opcionalmente múltiplos remotes)", inputSchema, handle };
|
|
44
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import { asToolError, asToolResult } from "../utils/errors.js";
|
|
3
|
+
|
|
4
|
+
const ajv = new Ajv({ allErrors: true });
|
|
5
|
+
|
|
6
|
+
export function createGitTagsTool(git) {
|
|
7
|
+
const inputSchema = {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
projectPath: { type: "string" },
|
|
11
|
+
action: { type: "string", enum: ["list", "create", "delete", "push"] },
|
|
12
|
+
tag: { type: "string" },
|
|
13
|
+
ref: { type: "string" },
|
|
14
|
+
message: { type: "string" }
|
|
15
|
+
},
|
|
16
|
+
required: ["projectPath", "action"],
|
|
17
|
+
additionalProperties: true
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async function handle(args) {
|
|
21
|
+
const validate = ajv.compile(inputSchema);
|
|
22
|
+
if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
23
|
+
const { projectPath, action } = args;
|
|
24
|
+
try {
|
|
25
|
+
if (action === "list") {
|
|
26
|
+
const tags = await git.listTags(projectPath);
|
|
27
|
+
return asToolResult({ tags });
|
|
28
|
+
}
|
|
29
|
+
if (action === "create") {
|
|
30
|
+
const tag = args.tag;
|
|
31
|
+
if (!tag) return asToolError("VALIDATION_ERROR", "tag é obrigatório");
|
|
32
|
+
await git.createTag(projectPath, tag, args.ref || "HEAD", args.message);
|
|
33
|
+
return asToolResult({ success: true, tag });
|
|
34
|
+
}
|
|
35
|
+
if (action === "delete") {
|
|
36
|
+
const tag = args.tag;
|
|
37
|
+
if (!tag) return asToolError("VALIDATION_ERROR", "tag é obrigatório");
|
|
38
|
+
await git.deleteTag(projectPath, tag);
|
|
39
|
+
return asToolResult({ success: true, tag });
|
|
40
|
+
}
|
|
41
|
+
if (action === "push") {
|
|
42
|
+
const tag = args.tag;
|
|
43
|
+
if (!tag) return asToolError("VALIDATION_ERROR", "tag é obrigatório");
|
|
44
|
+
await Promise.all([
|
|
45
|
+
git.pushTag(projectPath, "github", tag).catch(() => {}),
|
|
46
|
+
git.pushTag(projectPath, "gitea", tag).catch(() => {})
|
|
47
|
+
]);
|
|
48
|
+
return asToolResult({ success: true, tag, remotes: ["github", "gitea"] });
|
|
49
|
+
}
|
|
50
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return asToolError(e.code || "ERROR", e.message || String(e));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { name: "git-tags", description: "Gerencia tags e push paralelo", inputSchema, handle };
|
|
57
|
+
}
|
|
58
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import Ajv from "ajv";
|
|
2
|
+
import { MCPError, asToolError, asToolResult } from "../utils/errors.js";
|
|
3
|
+
import { getRepoNameFromPath } from "../utils/repoHelpers.js";
|
|
4
|
+
|
|
5
|
+
const ajv = new Ajv({ allErrors: true });
|
|
6
|
+
|
|
7
|
+
export function createGitWorkflowTool(pm, git) {
|
|
8
|
+
const inputSchema = {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
projectPath: { type: "string", description: "Caminho absoluto do projeto" },
|
|
12
|
+
action: { type: "string", enum: ["init", "status", "add", "remove", "commit", "push", "pull", "sync", "ensure-remotes"], description: "Ação" },
|
|
13
|
+
files: { type: "array", items: { type: "string" } },
|
|
14
|
+
message: { type: "string" }
|
|
15
|
+
},
|
|
16
|
+
required: ["projectPath", "action"],
|
|
17
|
+
additionalProperties: true
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async function handle(args) {
|
|
21
|
+
const validate = ajv.compile(inputSchema);
|
|
22
|
+
if (!validate(args || {})) {
|
|
23
|
+
return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
24
|
+
}
|
|
25
|
+
const { projectPath, action } = args;
|
|
26
|
+
try {
|
|
27
|
+
if (action === "init") {
|
|
28
|
+
await git.init(projectPath);
|
|
29
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
30
|
+
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true });
|
|
31
|
+
return asToolResult({ success: true, ensured, message: "Repositório inicializado" });
|
|
32
|
+
}
|
|
33
|
+
if (action === "status") {
|
|
34
|
+
const st = await git.status(projectPath);
|
|
35
|
+
return asToolResult(st);
|
|
36
|
+
}
|
|
37
|
+
if (action === "add") {
|
|
38
|
+
const files = Array.isArray(args.files) && args.files.length ? args.files : ["."];
|
|
39
|
+
await git.add(projectPath, files);
|
|
40
|
+
return asToolResult({ success: true, files });
|
|
41
|
+
}
|
|
42
|
+
if (action === "remove") {
|
|
43
|
+
const files = Array.isArray(args.files) ? args.files : [];
|
|
44
|
+
await git.remove(projectPath, files);
|
|
45
|
+
return asToolResult({ success: true, files });
|
|
46
|
+
}
|
|
47
|
+
if (action === "commit") {
|
|
48
|
+
const msg = args.message || "update";
|
|
49
|
+
const sha = await git.commit(projectPath, msg);
|
|
50
|
+
return asToolResult({ success: true, sha, message: msg });
|
|
51
|
+
}
|
|
52
|
+
if (action === "ensure-remotes") {
|
|
53
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
54
|
+
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true });
|
|
55
|
+
const ghOwner = await pm.getGitHubOwner();
|
|
56
|
+
const geOwner = await pm.getGiteaOwner();
|
|
57
|
+
const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
|
|
58
|
+
const base = pm.giteaUrl?.replace(/\/$/, "") || "";
|
|
59
|
+
const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
|
|
60
|
+
await git.ensureRemotes(projectPath, { githubUrl, giteaUrl });
|
|
61
|
+
return asToolResult({ success: true, ensured, remotes: { githubUrl, giteaUrl } });
|
|
62
|
+
}
|
|
63
|
+
if (action === "push") {
|
|
64
|
+
const branch = await git.getCurrentBranch(projectPath);
|
|
65
|
+
await git.pushParallel(projectPath, branch);
|
|
66
|
+
return asToolResult({ success: true, branch, remotes: ["github", "gitea"] });
|
|
67
|
+
}
|
|
68
|
+
if (action === "sync" || action === "pull") {
|
|
69
|
+
// Basic implementation: fetch + status; full merge/pull can be added
|
|
70
|
+
return asToolResult({ success: true, message: "Operação de pull/sync não-op realizada" });
|
|
71
|
+
}
|
|
72
|
+
return asToolError("VALIDATION_ERROR", `Ação não suportada: ${action}`);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return asToolError(e.code || "ERROR", e.message || String(e));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
name: "git-workflow",
|
|
80
|
+
description: "Operações Git locais e remotas em paralelo (GitHub + Gitea)",
|
|
81
|
+
inputSchema,
|
|
82
|
+
handle
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class MCPError extends Error {
|
|
2
|
+
constructor(code, message, data) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.code = code;
|
|
5
|
+
this.data = data;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function asToolResult(result) {
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
12
|
+
isError: false,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function asToolError(code, message, data) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: "text", text: JSON.stringify({ code, message, data }) }],
|
|
19
|
+
isError: true,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|