@andre.buzeli/git-mcp 15.12.5

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.
@@ -0,0 +1,492 @@
1
+ import Ajv from "ajv";
2
+ import axios from "axios";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import archiver from "archiver";
6
+ import { asToolError, asToolResult, errorToResponse, mapExternalError } from "../utils/errors.js";
7
+ import { getRepoNameFromPath, validateProjectPath, withRetry } from "../utils/repoHelpers.js";
8
+ import { runBoth } from "../utils/providerExec.js";
9
+
10
+ /**
11
+ * Compacta uma pasta em ZIP e retorna o caminho do arquivo temporário
12
+ * @param {string} folderPath - Caminho da pasta a compactar
13
+ * @param {string} zipName - Nome do arquivo ZIP (sem extensão)
14
+ * @returns {Promise<string>} Caminho do arquivo ZIP criado
15
+ */
16
+ async function compactFolder(folderPath, zipName) {
17
+ return new Promise((resolve, reject) => {
18
+ const outputPath = path.join(process.cwd(), 'temp_scripts', `${zipName}.zip`);
19
+ const output = fs.createWriteStream(outputPath);
20
+ const archive = archiver('zip', { zlib: { level: 9 } });
21
+
22
+ output.on('close', () => resolve(outputPath));
23
+ output.on('error', reject);
24
+ archive.on('error', reject);
25
+
26
+ archive.pipe(output);
27
+ archive.directory(folderPath, false);
28
+ archive.finalize();
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Faz upload de um asset para um release
34
+ * @param {Object} pm - Provider Manager
35
+ * @param {string} owner - Dono do repo
36
+ * @param {string} repo - Nome do repo
37
+ * @param {number} releaseId - ID do release
38
+ * @param {string} assetPath - Caminho do arquivo a fazer upload
39
+ * @param {string} assetName - Nome do asset (com extensão)
40
+ * @returns {Promise<Object>} Resultado do upload
41
+ */
42
+ async function uploadAsset(pm, owner, repo, releaseId, assetPath, assetName) {
43
+ const fileBuffer = fs.readFileSync(assetPath);
44
+ const fileSize = fs.statSync(assetPath).size;
45
+
46
+ const out = await runBoth(pm, {
47
+ github: async () => {
48
+ const response = await pm.github.request("POST /repos/{owner}/{repo}/releases/{release_id}/assets", {
49
+ owner,
50
+ repo,
51
+ release_id: releaseId,
52
+ name: assetName,
53
+ data: fileBuffer,
54
+ headers: {
55
+ 'content-type': 'application/octet-stream',
56
+ 'content-length': fileSize
57
+ }
58
+ });
59
+ return { ok: true, id: response.data.id, url: response.data.browser_download_url };
60
+ },
61
+ gitea: async () => {
62
+ const base = pm.giteaUrl.replace(/\/$/, "");
63
+
64
+ const response = await axios.post(
65
+ `${base}/api/v1/repos/${owner}/${repo}/releases/${releaseId}/assets?name=${encodeURIComponent(assetName)}`,
66
+ fileBuffer,
67
+ {
68
+ headers: {
69
+ 'Authorization': `token ${pm.giteaToken}`,
70
+ 'Content-Type': 'application/octet-stream',
71
+ 'Content-Length': fileSize
72
+ }
73
+ }
74
+ );
75
+ return { ok: true, id: response.data.id, url: response.data.browser_download_url };
76
+ }
77
+ });
78
+
79
+ return out;
80
+ }
81
+
82
+ const ajv = new Ajv({ allErrors: true });
83
+
84
+ export function createGitRemoteTool(pm, git) {
85
+ const inputSchema = {
86
+ type: "object",
87
+ properties: {
88
+ projectPath: {
89
+ type: "string",
90
+ description: "Caminho absoluto do diretório do projeto"
91
+ },
92
+ action: {
93
+ type: "string",
94
+ enum: [
95
+ "list", "list-all", "list-orgs", "ensure", "repo-delete",
96
+ "release-create", "topics-set", "milestone-create", "label-create",
97
+ "fork-create", "fork-list", "star-set", "star-unset",
98
+ "subscription-set", "subscription-unset", "contents-create"
99
+ ],
100
+ description: `Ação a executar:
101
+
102
+ CONFIGURAÇÃO:
103
+ CONFIGURAÇÃO:
104
+ - list: Lista remotes configurados (github, gitea, origin)
105
+ - list-all: Lista TODOS os repositórios da conta ou organização (GitHub + Gitea)
106
+ - list-orgs: Lista organizações disponíveis no GitHub e Gitea
107
+ - ensure: Cria repos no GitHub/Gitea e configura remotes (USE ESTE se push falhar)
108
+
109
+ REPOSITÓRIO:
110
+ - repo-delete: Deleta repositório no GitHub E Gitea (⚠️ PERIGOSO)
111
+ - release-create: Cria release/versão no GitHub e Gitea
112
+ - topics-set: Define tópicos/tags do repositório
113
+ - milestone-create: Cria milestone para organização
114
+ - label-create: Cria label para issues
115
+
116
+ SOCIAL:
117
+ - star-set: Adiciona estrela ao repo
118
+ - star-unset: Remove estrela
119
+ - fork-create: Cria fork do repositório
120
+ - fork-list: Lista forks existentes
121
+
122
+ NOTIFICAÇÕES:
123
+ - subscription-set: Ativa notificações do repo
124
+ - subscription-unset: Desativa notificações
125
+
126
+ ARQUIVOS VIA API:
127
+ - contents-create: Cria arquivo diretamente via API (sem git local)`
128
+ },
129
+ tag: {
130
+ type: "string",
131
+ description: "Tag para release-create. Ex: 'v1.0.0'"
132
+ },
133
+ name: {
134
+ type: "string",
135
+ description: "Nome da release, label, etc."
136
+ },
137
+ body: {
138
+ type: "string",
139
+ description: "Descrição/corpo da release"
140
+ },
141
+ assets: {
142
+ type: "array",
143
+ items: {
144
+ oneOf: [
145
+ { type: "string" }, // path simples: "dist/app.exe"
146
+ {
147
+ type: "object",
148
+ properties: {
149
+ path: { type: "string" }, // "dist/build"
150
+ name: { type: "string" } // "MeuApp-Windows" (sem extensao)
151
+ },
152
+ required: ["path"]
153
+ }
154
+ ]
155
+ },
156
+ description: "Assets para upload. String = nome automatico, Objeto = nome custom. Pastas são compactadas automaticamente"
157
+ },
158
+ topics: {
159
+ type: "array",
160
+ items: { type: "string" },
161
+ description: "Lista de tópicos para topics-set. Ex: ['javascript', 'nodejs', 'mcp']"
162
+ },
163
+ path: {
164
+ type: "string",
165
+ description: "Caminho do arquivo para contents-create"
166
+ },
167
+ content: {
168
+ type: "string",
169
+ description: "Conteúdo do arquivo para contents-create"
170
+ },
171
+ branch: {
172
+ type: "string",
173
+ description: "Branch alvo para contents-create. Default: main"
174
+ },
175
+ color: {
176
+ type: "string",
177
+ description: "Cor do label em hex (sem #). Ex: 'ff0000' para vermelho"
178
+ },
179
+ title: {
180
+ type: "string",
181
+ description: "Título do milestone"
182
+ },
183
+ isPublic: {
184
+ type: "boolean",
185
+ description: "Se true, repositório será PÚBLICO. Default: false (privado). Aplica-se a action='ensure'"
186
+ },
187
+ organization: {
188
+ type: "string",
189
+ description: "Opcional. Nome da organização no GitHub/Gitea. Se informado, operações são feitas na org em vez da conta pessoal. Ex: 'automacao-casa', 'cliente-joao'"
190
+ }
191
+ },
192
+ required: ["projectPath", "action"],
193
+ additionalProperties: false
194
+ };
195
+
196
+ const description = `Operações em repositórios remotos GitHub e Gitea.
197
+
198
+ AÇÕES MAIS USADAS:
199
+ - ensure: Configurar remotes e criar repos (ESSENCIAL antes de push)
200
+ - list: Ver remotes configurados
201
+ - release-create: Publicar nova versão
202
+
203
+ EXECUÇÃO PARALELA:
204
+ Todas as operações são executadas em AMBOS os providers (GitHub + Gitea) simultaneamente.
205
+
206
+ QUANDO USAR:
207
+ - Se git-workflow push falhar: use action='ensure'
208
+ - Para publicar release: use action='release-create'
209
+ - Para configurar tópicos: use action='topics-set'`;
210
+
211
+ async function handle(args) {
212
+ const validate = ajv.compile(inputSchema);
213
+ if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
214
+ const { projectPath, action } = args;
215
+ try {
216
+ validateProjectPath(projectPath);
217
+ if (action === "list") {
218
+ const remotes = await git.listRemotes(projectPath);
219
+
220
+ // Debug info: calculate what URLs should be
221
+ let repoName = getRepoNameFromPath(projectPath);
222
+ if (repoName === "GIT_MCP") repoName = "git-mcp";
223
+ const calculated = await pm.getRemoteUrls(repoName, args.organization);
224
+
225
+ return asToolResult({
226
+ remotes,
227
+ configured: remotes.length > 0,
228
+ hasGithub: remotes.some(r => r.remote === "github"),
229
+ hasGitea: remotes.some(r => r.remote === "gitea"),
230
+ debug: {
231
+ repoName,
232
+ calculatedUrls: calculated,
233
+ env: {
234
+ hasGithubToken: !!pm.githubToken,
235
+ hasGiteaToken: !!pm.giteaToken,
236
+ giteaUrl: pm.giteaUrl
237
+ }
238
+ },
239
+ message: remotes.length === 0 ? "Nenhum remote configurado. Use action='ensure' para configurar." : undefined
240
+ });
241
+ }
242
+
243
+ if (action === "list-all") {
244
+ const repos = await pm.listAllRepos({ organization: args.organization });
245
+ const summary = [];
246
+ if (repos.github?.length) summary.push(`${repos.github.length} GitHub repos`);
247
+ if (repos.gitea?.length) summary.push(`${repos.gitea.length} Gitea repos`);
248
+
249
+ return asToolResult({
250
+ success: true,
251
+ organization: args.organization || undefined,
252
+ summary: summary.join(", "),
253
+ github: repos.github,
254
+ gitea: repos.gitea
255
+ });
256
+ }
257
+ if (action === "list-orgs") {
258
+ const orgs = await pm.listOrgs();
259
+ const summary = [];
260
+ if (orgs.github?.length) summary.push(`${orgs.github.length} GitHub orgs`);
261
+ if (orgs.gitea?.length) summary.push(`${orgs.gitea.length} Gitea orgs`);
262
+
263
+ return asToolResult({
264
+ success: true,
265
+ summary: summary.length > 0 ? summary.join(", ") : "Nenhuma organização encontrada",
266
+ github: orgs.github,
267
+ gitea: orgs.gitea,
268
+ _aiContext: {
269
+ hint: "Use o nome da organização no parâmetro 'organization' das outras actions para criar/gerenciar repos dentro da org"
270
+ }
271
+ });
272
+ }
273
+ if (action === "ensure") {
274
+ const repo = getRepoNameFromPath(projectPath);
275
+ const isPublic = args.isPublic === true; // Default: privado
276
+ const organization = args.organization || undefined;
277
+ const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic, organization });
278
+ const urls = await pm.getRemoteUrls(repo, organization);
279
+ await git.ensureRemotes(projectPath, { githubUrl: urls.github || "", giteaUrl: urls.gitea || "" });
280
+ const remotes = await git.listRemotes(projectPath);
281
+ return asToolResult({
282
+ success: true,
283
+ isPrivate: !isPublic,
284
+ organization: organization || undefined,
285
+ ensured,
286
+ remotes,
287
+ urls,
288
+ message: "Remotes configurados. Agora pode usar git-workflow push." + (organization ? ` [org: ${organization}]` : "")
289
+ });
290
+ }
291
+ if (action === "repo-delete") {
292
+ const repo = getRepoNameFromPath(projectPath);
293
+ const out = await runBoth(pm, {
294
+ github: async (owner) => { await pm.github.rest.repos.delete({ owner, repo }); return { ok: true }; },
295
+ 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 }; }
296
+ }, { organization: args.organization });
297
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), deleted: repo, providers: out, warning: "Repositório deletado permanentemente!" });
298
+ }
299
+ if (action === "release-create") {
300
+ const repo = getRepoNameFromPath(projectPath);
301
+ if (!args.tag) return asToolError("MISSING_PARAMETER", "tag é obrigatório para criar release", { parameter: "tag", example: "v1.0.0" });
302
+ const tag = args.tag;
303
+ const name = args.name || tag;
304
+ const body = args.body || "";
305
+
306
+ // Criar release primeiro
307
+ const out = await runBoth(pm, {
308
+ 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, url: r.data.html_url }; },
309
+ 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 }; }
310
+ }, { organization: args.organization });
311
+
312
+ const releaseCreated = !!(out.github?.ok || out.gitea?.ok);
313
+ if (!releaseCreated) {
314
+ return asToolResult({ success: false, tag, name, providers: out, error: "Falha ao criar release" });
315
+ }
316
+
317
+ // Processar assets se fornecidos
318
+ let assetsUploaded = [];
319
+ if (args.assets && args.assets.length > 0) {
320
+ const tempFiles = []; // Arquivos temporários a serem deletados
321
+ const [ghOwner, geOwner] = await Promise.all([
322
+ pm.getGitHubOwner().catch(() => ""),
323
+ pm.getGiteaOwner().catch(() => "")
324
+ ]);
325
+
326
+ for (const asset of args.assets) {
327
+ try {
328
+ const assetPath = typeof asset === 'string' ? asset : asset.path;
329
+ const customName = typeof asset === 'object' ? asset.name : null;
330
+
331
+ // Resolver caminho absoluto
332
+ const absolutePath = path.resolve(projectPath, assetPath);
333
+
334
+ // Verificar se existe
335
+ if (!fs.existsSync(absolutePath)) {
336
+ assetsUploaded.push({ path: assetPath, error: "Arquivo/pasta não encontrado" });
337
+ continue;
338
+ }
339
+
340
+ const stats = fs.statSync(absolutePath);
341
+ let uploadPath = absolutePath;
342
+ let uploadName = customName;
343
+ let isTempFile = false;
344
+
345
+ if (stats.isDirectory()) {
346
+ // Compactar pasta
347
+ const zipName = customName || path.basename(absolutePath);
348
+ const zipPath = await compactFolder(absolutePath, zipName);
349
+ uploadPath = zipPath;
350
+ uploadName = `${zipName}.zip`;
351
+ tempFiles.push(zipPath);
352
+ isTempFile = true;
353
+ } else {
354
+ // Arquivo direto
355
+ if (!uploadName) {
356
+ uploadName = path.basename(absolutePath);
357
+ } else {
358
+ // Adicionar extensão se não tiver
359
+ const ext = path.extname(absolutePath);
360
+ if (!uploadName.includes('.')) {
361
+ uploadName += ext;
362
+ }
363
+ }
364
+ }
365
+
366
+ // Upload para GitHub e Gitea
367
+ const uploadResult = await uploadAsset(pm, ghOwner || geOwner, repo, out.github?.id || out.gitea?.id, uploadPath, uploadName);
368
+
369
+ assetsUploaded.push({
370
+ path: assetPath,
371
+ name: uploadName,
372
+ uploaded: !!(uploadResult.github?.ok || uploadResult.gitea?.ok),
373
+ providers: uploadResult
374
+ });
375
+
376
+ // Deletar arquivo temporário se foi criado
377
+ if (isTempFile && tempFiles.includes(uploadPath)) {
378
+ try {
379
+ fs.unlinkSync(uploadPath);
380
+ tempFiles.splice(tempFiles.indexOf(uploadPath), 1);
381
+ } catch (e) {
382
+ console.warn(`Falha ao deletar arquivo temporário: ${uploadPath}`);
383
+ }
384
+ }
385
+
386
+ } catch (error) {
387
+ assetsUploaded.push({ path: typeof asset === 'string' ? asset : asset.path, error: String(error.message || error) });
388
+ }
389
+ }
390
+ }
391
+
392
+ return asToolResult({
393
+ success: releaseCreated,
394
+ tag,
395
+ name,
396
+ providers: out,
397
+ assets: assetsUploaded.length > 0 ? assetsUploaded : undefined
398
+ });
399
+ }
400
+ if (action === "topics-set") {
401
+ const repo = getRepoNameFromPath(projectPath);
402
+ const topics = Array.isArray(args.topics) ? args.topics : [];
403
+ if (topics.length === 0) return asToolError("MISSING_PARAMETER", "topics é obrigatório", { parameter: "topics", example: ["javascript", "nodejs"] });
404
+ const out = await runBoth(pm, {
405
+ 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 }; },
406
+ 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 }; }
407
+ }, { organization: args.organization });
408
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), topics, providers: out });
409
+ }
410
+ if (action === "milestone-create") {
411
+ const repo = getRepoNameFromPath(projectPath);
412
+ const title = args.title || args.name || "v1.0";
413
+ const out = await runBoth(pm, {
414
+ github: async (owner) => { const r = await pm.github.request("POST /repos/{owner}/{repo}/milestones", { owner, repo, title }); return { ok: true, id: r.data?.id }; },
415
+ 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 }; }
416
+ }, { organization: args.organization });
417
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), title, providers: out });
418
+ }
419
+ if (action === "label-create") {
420
+ const repo = getRepoNameFromPath(projectPath);
421
+ const name = args.name || "bug";
422
+ const color = String(args.color || "ff0000").replace(/^#/, "");
423
+ const out = await runBoth(pm, {
424
+ github: async (owner) => { await pm.github.request("POST /repos/{owner}/{repo}/labels", { owner, repo, name, color }); return { ok: true }; },
425
+ 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 }; }
426
+ }, { organization: args.organization });
427
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), label: { name, color }, providers: out });
428
+ }
429
+ if (action === "fork-create") {
430
+ const repo = getRepoNameFromPath(projectPath);
431
+ const out = await runBoth(pm, {
432
+ github: async (owner) => { const r = await pm.github.rest.repos.createFork({ owner, repo }); return { ok: true, full_name: r.data?.full_name }; },
433
+ 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 }; }
434
+ }, { organization: args.organization });
435
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
436
+ }
437
+ if (action === "fork-list") {
438
+ const repo = getRepoNameFromPath(projectPath);
439
+ const out = await runBoth(pm, {
440
+ github: async (owner) => { const r = await pm.github.rest.repos.listForks({ owner, repo }); return { ok: true, count: r.data.length }; },
441
+ 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 }; }
442
+ }, { organization: args.organization });
443
+ return asToolResult({ providers: out });
444
+ }
445
+ if (action === "star-set" || action === "star-unset") {
446
+ const repo = getRepoNameFromPath(projectPath);
447
+ const out = await runBoth(pm, {
448
+ github: async (owner) => {
449
+ if (action === "star-set") { await pm.github.request("PUT /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true }; }
450
+ await pm.github.request("DELETE /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true };
451
+ },
452
+ gitea: async (owner) => {
453
+ const base = pm.giteaUrl.replace(/\/$/, "");
454
+ if (action === "star-set") { await axios.put(`${base}/api/v1/user/starred/${owner}/${repo}`, {}, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
455
+ await axios.delete(`${base}/api/v1/user/starred/${owner}/${repo}`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true };
456
+ }
457
+ }, { organization: args.organization });
458
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), action, providers: out });
459
+ }
460
+ if (action === "subscription-set" || action === "subscription-unset") {
461
+ const repo = getRepoNameFromPath(projectPath);
462
+ const out = await runBoth(pm, {
463
+ 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 }; },
464
+ 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 }; }
465
+ }, { organization: args.organization });
466
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), action, providers: out });
467
+ }
468
+ if (action === "contents-create") {
469
+ const repo = getRepoNameFromPath(projectPath);
470
+ if (!args.path) return asToolError("MISSING_PARAMETER", "path é obrigatório", { parameter: "path" });
471
+ if (!args.content) return asToolError("MISSING_PARAMETER", "content é obrigatório", { parameter: "content" });
472
+ const filePath = args.path;
473
+ const message = args.message || `Add ${filePath}`;
474
+ const data = args.content;
475
+ const branch = args.branch || "main";
476
+ const b64 = Buffer.from(data, "utf8").toString("base64");
477
+ const out = await runBoth(pm, {
478
+ github: async (owner) => { await pm.github.request("PUT /repos/{owner}/{repo}/contents/{path}", { owner, repo, path: filePath, message, content: b64, branch }); return { ok: true }; },
479
+ 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 }; }
480
+ }, { organization: args.organization });
481
+ return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), path: filePath, branch, providers: out });
482
+ }
483
+ return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
484
+ availableActions: ["list", "list-all", "list-orgs", "ensure", "repo-delete", "release-create", "topics-set", "milestone-create", "label-create", "fork-create", "fork-list", "star-set", "star-unset", "subscription-set", "subscription-unset", "contents-create"]
485
+ });
486
+ } catch (e) {
487
+ return errorToResponse(e);
488
+ }
489
+ }
490
+
491
+ return { name: "git-remote", description, inputSchema, handle };
492
+ }
@@ -0,0 +1,105 @@
1
+ import Ajv from "ajv";
2
+ import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
3
+ import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
4
+
5
+ const ajv = new Ajv({ allErrors: true });
6
+
7
+ export function createGitResetTool(git) {
8
+ const inputSchema = {
9
+ type: "object",
10
+ properties: {
11
+ projectPath: {
12
+ type: "string",
13
+ description: "Caminho absoluto do diretório do projeto"
14
+ },
15
+ action: {
16
+ type: "string",
17
+ enum: ["soft", "mixed", "hard", "hard-clean"],
18
+ description: `Tipo de reset a executar:
19
+ - soft: Desfaz commits mas MANTÉM mudanças staged (pronto para re-commit)
20
+ - mixed: Desfaz commits e unstage, mas MANTÉM mudanças no working directory
21
+ - hard: Desfaz commits e DESCARTA todas as mudanças (PERIGOSO - dados perdidos)
22
+ - hard-clean: Como hard, mas também REMOVE arquivos não rastreados (MUITO PERIGOSO)`
23
+ },
24
+ ref: {
25
+ type: "string",
26
+ description: "Referência para reset. Ex: 'HEAD~1' (volta 1 commit), 'HEAD~2' (volta 2), ou SHA específico"
27
+ }
28
+ },
29
+ required: ["projectPath", "action", "ref"],
30
+ additionalProperties: false
31
+ };
32
+
33
+ const description = `Reset Git - desfaz commits e/ou mudanças.
34
+
35
+ ⚠️ CUIDADO: Reset pode causar perda de dados!
36
+
37
+ TIPOS DE RESET:
38
+ - soft: Seguro - mantém tudo, apenas move HEAD
39
+ - mixed: Moderado - remove do staging mas mantém arquivos
40
+ - hard: PERIGOSO - descarta todas as mudanças
41
+ - hard-clean: MUITO PERIGOSO - descarta mudanças E remove arquivos não rastreados
42
+
43
+ EXEMPLOS COMUNS:
44
+ - Desfazer último commit (manter mudanças): action='soft' ref='HEAD~1'
45
+ - Desfazer último commit (unstage): action='mixed' ref='HEAD~1'
46
+ - Descartar tudo e voltar ao commit anterior: action='hard' ref='HEAD~1'
47
+ - Limpar completamente o workspace: action='hard-clean' ref='HEAD'
48
+
49
+ REFERÊNCIAS:
50
+ - HEAD~1: Um commit atrás
51
+ - HEAD~2: Dois commits atrás
52
+ - abc1234: SHA específico do commit`;
53
+
54
+ async function handle(args) {
55
+ const validate = ajv.compile(inputSchema);
56
+ if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
57
+ const { projectPath, action, ref } = args;
58
+ try {
59
+ validateProjectPath(projectPath);
60
+ // Verificar se há commits suficientes para HEAD~N
61
+ if (ref.match(/HEAD~(\d+)/)) {
62
+ const match = ref.match(/HEAD~(\d+)/);
63
+ const steps = parseInt(match[1], 10);
64
+ const log = await git.log(projectPath, { maxCount: steps + 2 });
65
+ if (log.length <= steps) {
66
+ return asToolError("INSUFFICIENT_HISTORY", `Histórico insuficiente para ${ref}`, {
67
+ requestedSteps: steps,
68
+ availableCommits: log.length,
69
+ suggestion: `Use HEAD~${log.length - 1} no máximo, ou um SHA específico`,
70
+ recentCommits: log.slice(0, 5).map(c => ({ sha: c.sha.substring(0, 7), message: c.message.split("\n")[0] }))
71
+ });
72
+ }
73
+ }
74
+
75
+ if (action === "soft") {
76
+ await withRetry(() => git.resetSoft(projectPath, ref), 3, "reset-soft");
77
+ return asToolResult({ success: true, action: "soft", ref, message: "Commits desfeitos, mudanças mantidas staged" });
78
+ }
79
+ if (action === "mixed") {
80
+ await withRetry(() => git.resetMixed(projectPath, ref), 3, "reset-mixed");
81
+ return asToolResult({ success: true, action: "mixed", ref, message: "Commits desfeitos, mudanças mantidas no working directory" });
82
+ }
83
+ if (action === "hard") {
84
+ await withRetry(() => git.resetHard(projectPath, ref), 3, "reset-hard");
85
+ return asToolResult({ success: true, action: "hard", ref, message: "⚠️ Reset hard executado - mudanças descartadas" });
86
+ }
87
+ if (action === "hard-clean") {
88
+ const result = await withRetry(() => git.resetHardClean(projectPath, ref), 3, "reset-hard-clean");
89
+ return asToolResult({
90
+ success: true,
91
+ action: "hard-clean",
92
+ ref,
93
+ cleanedFiles: result.cleaned,
94
+ message: `⚠️ Reset hard-clean executado - mudanças descartadas e ${result.cleaned.length} arquivo(s) não rastreados removidos`
95
+ });
96
+ }
97
+
98
+ return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, { availableActions: ["soft", "mixed", "hard", "hard-clean"] });
99
+ } catch (e) {
100
+ return errorToResponse(e);
101
+ }
102
+ }
103
+
104
+ return { name: "git-reset", description, inputSchema, handle };
105
+ }