@andrebuzeli/git-mcp 15.8.0 → 15.8.1
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 +2 -1
- package/src/prompts/index.js +46 -8
- package/src/tools/git-remote.js +182 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andrebuzeli/git-mcp",
|
|
3
|
-
"version": "15.8.
|
|
3
|
+
"version": "15.8.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "MCP server para Git com operações locais e sincronização paralela GitHub/Gitea",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"@modelcontextprotocol/sdk": "^0.4.0",
|
|
36
36
|
"@octokit/rest": "^20.0.0",
|
|
37
37
|
"ajv": "^8.12.0",
|
|
38
|
+
"archiver": "^7.0.1",
|
|
38
39
|
"axios": "^1.7.7"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
package/src/prompts/index.js
CHANGED
|
@@ -430,11 +430,42 @@ Analise o histórico e sugira qual commit pode ter introduzido o bug.`
|
|
|
430
430
|
},
|
|
431
431
|
|
|
432
432
|
"git-release": {
|
|
433
|
-
getMessages: (context) =>
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
433
|
+
getMessages: (context) => {
|
|
434
|
+
// Analisa mensagem do usuário para detectar instruções sobre assets
|
|
435
|
+
const userMessage = context.userMessage || "";
|
|
436
|
+
let assetsInstructions = "";
|
|
437
|
+
let detectedAssets = [];
|
|
438
|
+
|
|
439
|
+
// Detecta menções de upload/pasta/asset
|
|
440
|
+
const assetPatterns = [
|
|
441
|
+
/(?:upload|envia|adiciona?) (?:tamb[eé]m? )?(?:a |o |da |do )?(?:pasta|arquivo|asset) (.+?)(?: com (?:o |a )?nome (.+?))?(?:$|[.!?])/gi,
|
|
442
|
+
/(?:inclu[íi]|adiciona?) (.+?) (?:como |no |ao )?asset/gi,
|
|
443
|
+
/(?:compacta|zipa) (.+?)(?: como (.+?))?(?: e envia|$)/gi
|
|
444
|
+
];
|
|
445
|
+
|
|
446
|
+
for (const pattern of assetPatterns) {
|
|
447
|
+
let match;
|
|
448
|
+
while ((match = pattern.exec(userMessage)) !== null) {
|
|
449
|
+
const assetPath = match[1]?.trim();
|
|
450
|
+
const assetName = match[2]?.trim();
|
|
451
|
+
|
|
452
|
+
if (assetPath) {
|
|
453
|
+
if (assetName) {
|
|
454
|
+
detectedAssets.push(`{ path: "${assetPath}", name: "${assetName}" }`);
|
|
455
|
+
assetsInstructions += `- Asset customizado: \`${assetPath}\` → \`${assetName}\`\n`;
|
|
456
|
+
} else {
|
|
457
|
+
detectedAssets.push(`"${assetPath}"`);
|
|
458
|
+
assetsInstructions += `- Asset automático: \`${assetPath}\`\n`;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return [{
|
|
465
|
+
role: "user",
|
|
466
|
+
content: {
|
|
467
|
+
type: "text",
|
|
468
|
+
text: `# Criar Release do Projeto
|
|
438
469
|
|
|
439
470
|
Você vai criar uma nova release deste projeto.
|
|
440
471
|
|
|
@@ -447,6 +478,9 @@ Você vai criar uma nova release deste projeto.
|
|
|
447
478
|
## Commits para esta Release
|
|
448
479
|
${context.recentCommits?.map(c => `- ${c.shortSha}: ${c.message}`).join("\n") || "Use git-history para ver commits"}
|
|
449
480
|
|
|
481
|
+
## Assets para Upload
|
|
482
|
+
${assetsInstructions || "Nenhum asset específico solicitado"}
|
|
483
|
+
|
|
450
484
|
## Passos para Release
|
|
451
485
|
|
|
452
486
|
1. **Determinar versão**: Baseado nos commits, sugira a próxima versão (semver)
|
|
@@ -464,6 +498,7 @@ ${context.recentCommits?.map(c => `- ${c.shortSha}: ${c.message}`).join("\n") ||
|
|
|
464
498
|
- tag: "vX.Y.Z"
|
|
465
499
|
- name: "Release vX.Y.Z"
|
|
466
500
|
- body: Release notes geradas
|
|
501
|
+
${detectedAssets.length > 0 ? ` - assets: [${detectedAssets.join(', ')}]` : ''}
|
|
467
502
|
|
|
468
503
|
## Template de Release Notes
|
|
469
504
|
|
|
@@ -482,9 +517,12 @@ ${context.recentCommits?.map(c => `- ${c.shortSha}: ${c.message}`).join("\n") ||
|
|
|
482
517
|
- Mudança 1
|
|
483
518
|
\`\`\`
|
|
484
519
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
520
|
+
${assetsInstructions ? `## 📦 Assets Inclusos\n${assetsInstructions}` : ''}
|
|
521
|
+
|
|
522
|
+
Analise os commits e execute os passos para criar a release${assetsInstructions ? ', incluindo os assets solicitados' : ''}. UTILIZANDO APENAS AS TOOLS DO GIT-MCP.`
|
|
523
|
+
}
|
|
524
|
+
}];
|
|
525
|
+
}
|
|
488
526
|
},
|
|
489
527
|
|
|
490
528
|
"git-update": {
|
package/src/tools/git-remote.js
CHANGED
|
@@ -1,9 +1,84 @@
|
|
|
1
1
|
import Ajv from "ajv";
|
|
2
2
|
import axios from "axios";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import archiver from "archiver";
|
|
3
6
|
import { asToolError, asToolResult, errorToResponse, mapExternalError } from "../utils/errors.js";
|
|
4
7
|
import { getRepoNameFromPath, validateProjectPath } from "../utils/repoHelpers.js";
|
|
5
8
|
import { runBoth } from "../utils/providerExec.js";
|
|
6
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
|
+
|
|
7
82
|
const ajv = new Ajv({ allErrors: true });
|
|
8
83
|
|
|
9
84
|
export function createGitRemoteTool(pm, git) {
|
|
@@ -62,6 +137,23 @@ ARQUIVOS VIA API:
|
|
|
62
137
|
type: "string",
|
|
63
138
|
description: "Descrição/corpo da release"
|
|
64
139
|
},
|
|
140
|
+
assets: {
|
|
141
|
+
type: "array",
|
|
142
|
+
items: {
|
|
143
|
+
oneOf: [
|
|
144
|
+
{ type: "string" }, // path simples: "dist/app.exe"
|
|
145
|
+
{
|
|
146
|
+
type: "object",
|
|
147
|
+
properties: {
|
|
148
|
+
path: { type: "string" }, // "dist/build"
|
|
149
|
+
name: { type: "string" } // "MeuApp-Windows" (sem extensao)
|
|
150
|
+
},
|
|
151
|
+
required: ["path"]
|
|
152
|
+
}
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
description: "Assets para upload. String = nome automatico, Objeto = nome custom. Pastas são compactadas automaticamente"
|
|
156
|
+
},
|
|
65
157
|
topics: {
|
|
66
158
|
type: "array",
|
|
67
159
|
items: { type: "string" },
|
|
@@ -190,11 +282,100 @@ QUANDO USAR:
|
|
|
190
282
|
const tag = args.tag;
|
|
191
283
|
const name = args.name || tag;
|
|
192
284
|
const body = args.body || "";
|
|
285
|
+
|
|
286
|
+
// Criar release primeiro
|
|
193
287
|
const out = await runBoth(pm, {
|
|
194
288
|
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 }; },
|
|
195
289
|
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 }; }
|
|
196
290
|
});
|
|
197
|
-
|
|
291
|
+
|
|
292
|
+
const releaseCreated = !!(out.github?.ok || out.gitea?.ok);
|
|
293
|
+
if (!releaseCreated) {
|
|
294
|
+
return asToolResult({ success: false, tag, name, providers: out, error: "Falha ao criar release" });
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Processar assets se fornecidos
|
|
298
|
+
let assetsUploaded = [];
|
|
299
|
+
if (args.assets && args.assets.length > 0) {
|
|
300
|
+
const tempFiles = []; // Arquivos temporários a serem deletados
|
|
301
|
+
const [ghOwner, geOwner] = await Promise.all([
|
|
302
|
+
pm.getGitHubOwner().catch(() => ""),
|
|
303
|
+
pm.getGiteaOwner().catch(() => "")
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
for (const asset of args.assets) {
|
|
307
|
+
try {
|
|
308
|
+
const assetPath = typeof asset === 'string' ? asset : asset.path;
|
|
309
|
+
const customName = typeof asset === 'object' ? asset.name : null;
|
|
310
|
+
|
|
311
|
+
// Resolver caminho absoluto
|
|
312
|
+
const absolutePath = path.resolve(projectPath, assetPath);
|
|
313
|
+
|
|
314
|
+
// Verificar se existe
|
|
315
|
+
if (!fs.existsSync(absolutePath)) {
|
|
316
|
+
assetsUploaded.push({ path: assetPath, error: "Arquivo/pasta não encontrado" });
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const stats = fs.statSync(absolutePath);
|
|
321
|
+
let uploadPath = absolutePath;
|
|
322
|
+
let uploadName = customName;
|
|
323
|
+
let isTempFile = false;
|
|
324
|
+
|
|
325
|
+
if (stats.isDirectory()) {
|
|
326
|
+
// Compactar pasta
|
|
327
|
+
const zipName = customName || path.basename(absolutePath);
|
|
328
|
+
const zipPath = await compactFolder(absolutePath, zipName);
|
|
329
|
+
uploadPath = zipPath;
|
|
330
|
+
uploadName = `${zipName}.zip`;
|
|
331
|
+
tempFiles.push(zipPath);
|
|
332
|
+
isTempFile = true;
|
|
333
|
+
} else {
|
|
334
|
+
// Arquivo direto
|
|
335
|
+
if (!uploadName) {
|
|
336
|
+
uploadName = path.basename(absolutePath);
|
|
337
|
+
} else {
|
|
338
|
+
// Adicionar extensão se não tiver
|
|
339
|
+
const ext = path.extname(absolutePath);
|
|
340
|
+
if (!uploadName.includes('.')) {
|
|
341
|
+
uploadName += ext;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Upload para GitHub e Gitea
|
|
347
|
+
const uploadResult = await uploadAsset(pm, ghOwner || geOwner, repo, out.github?.id || out.gitea?.id, uploadPath, uploadName);
|
|
348
|
+
|
|
349
|
+
assetsUploaded.push({
|
|
350
|
+
path: assetPath,
|
|
351
|
+
name: uploadName,
|
|
352
|
+
uploaded: !!(uploadResult.github?.ok || uploadResult.gitea?.ok),
|
|
353
|
+
providers: uploadResult
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Deletar arquivo temporário se foi criado
|
|
357
|
+
if (isTempFile && tempFiles.includes(uploadPath)) {
|
|
358
|
+
try {
|
|
359
|
+
fs.unlinkSync(uploadPath);
|
|
360
|
+
tempFiles.splice(tempFiles.indexOf(uploadPath), 1);
|
|
361
|
+
} catch (e) {
|
|
362
|
+
console.warn(`Falha ao deletar arquivo temporário: ${uploadPath}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
} catch (error) {
|
|
367
|
+
assetsUploaded.push({ path: typeof asset === 'string' ? asset : asset.path, error: String(error.message || error) });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return asToolResult({
|
|
373
|
+
success: releaseCreated,
|
|
374
|
+
tag,
|
|
375
|
+
name,
|
|
376
|
+
providers: out,
|
|
377
|
+
assets: assetsUploaded.length > 0 ? assetsUploaded : undefined
|
|
378
|
+
});
|
|
198
379
|
}
|
|
199
380
|
if (action === "topics-set") {
|
|
200
381
|
const repo = getRepoNameFromPath(projectPath);
|