@andrebuzeli/git-mcp 15.8.3 → 15.8.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.
- package/README.md +39 -125
- package/package.json +27 -44
- package/src/index.js +146 -139
- package/src/providers/providerManager.js +203 -203
- package/src/tools/git-diff.js +137 -126
- package/src/tools/git-help.js +285 -285
- package/src/tools/git-remote.js +472 -472
- package/src/tools/git-workflow.js +403 -403
- package/src/utils/env.js +104 -104
- package/src/utils/errors.js +431 -431
- package/src/utils/gitAdapter.js +932 -932
- package/src/utils/hooks.js +255 -255
- package/src/utils/metrics.js +198 -198
- package/src/utils/providerExec.js +58 -58
- package/src/utils/repoHelpers.js +160 -160
- package/src/utils/retry.js +123 -123
- package/install.sh +0 -68
package/src/tools/git-remote.js
CHANGED
|
@@ -1,472 +1,472 @@
|
|
|
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 } 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", "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 (GitHub + Gitea)
|
|
106
|
-
- ensure: Cria repos no GitHub/Gitea e configura remotes (USE ESTE se push falhar)
|
|
107
|
-
|
|
108
|
-
REPOSITÓRIO:
|
|
109
|
-
- repo-delete: Deleta repositório no GitHub E Gitea (⚠️ PERIGOSO)
|
|
110
|
-
- release-create: Cria release/versão no GitHub e Gitea
|
|
111
|
-
- topics-set: Define tópicos/tags do repositório
|
|
112
|
-
- milestone-create: Cria milestone para organização
|
|
113
|
-
- label-create: Cria label para issues
|
|
114
|
-
|
|
115
|
-
SOCIAL:
|
|
116
|
-
- star-set: Adiciona estrela ao repo
|
|
117
|
-
- star-unset: Remove estrela
|
|
118
|
-
- fork-create: Cria fork do repositório
|
|
119
|
-
- fork-list: Lista forks existentes
|
|
120
|
-
|
|
121
|
-
NOTIFICAÇÕES:
|
|
122
|
-
- subscription-set: Ativa notificações do repo
|
|
123
|
-
- subscription-unset: Desativa notificações
|
|
124
|
-
|
|
125
|
-
ARQUIVOS VIA API:
|
|
126
|
-
- contents-create: Cria arquivo diretamente via API (sem git local)`
|
|
127
|
-
},
|
|
128
|
-
tag: {
|
|
129
|
-
type: "string",
|
|
130
|
-
description: "Tag para release-create. Ex: 'v1.0.0'"
|
|
131
|
-
},
|
|
132
|
-
name: {
|
|
133
|
-
type: "string",
|
|
134
|
-
description: "Nome da release, label, etc."
|
|
135
|
-
},
|
|
136
|
-
body: {
|
|
137
|
-
type: "string",
|
|
138
|
-
description: "Descrição/corpo da release"
|
|
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
|
-
},
|
|
157
|
-
topics: {
|
|
158
|
-
type: "array",
|
|
159
|
-
items: { type: "string" },
|
|
160
|
-
description: "Lista de tópicos para topics-set. Ex: ['javascript', 'nodejs', 'mcp']"
|
|
161
|
-
},
|
|
162
|
-
path: {
|
|
163
|
-
type: "string",
|
|
164
|
-
description: "Caminho do arquivo para contents-create"
|
|
165
|
-
},
|
|
166
|
-
content: {
|
|
167
|
-
type: "string",
|
|
168
|
-
description: "Conteúdo do arquivo para contents-create"
|
|
169
|
-
},
|
|
170
|
-
branch: {
|
|
171
|
-
type: "string",
|
|
172
|
-
description: "Branch alvo para contents-create. Default: main"
|
|
173
|
-
},
|
|
174
|
-
color: {
|
|
175
|
-
type: "string",
|
|
176
|
-
description: "Cor do label em hex (sem #). Ex: 'ff0000' para vermelho"
|
|
177
|
-
},
|
|
178
|
-
title: {
|
|
179
|
-
type: "string",
|
|
180
|
-
description: "Título do milestone"
|
|
181
|
-
},
|
|
182
|
-
isPublic: {
|
|
183
|
-
type: "boolean",
|
|
184
|
-
description: "Se true, repositório será PÚBLICO. Default: false (privado). Aplica-se a action='ensure'"
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
required: ["projectPath", "action"],
|
|
188
|
-
additionalProperties: false
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const description = `Operações em repositórios remotos GitHub e Gitea.
|
|
192
|
-
|
|
193
|
-
AÇÕES MAIS USADAS:
|
|
194
|
-
- ensure: Configurar remotes e criar repos (ESSENCIAL antes de push)
|
|
195
|
-
- list: Ver remotes configurados
|
|
196
|
-
- release-create: Publicar nova versão
|
|
197
|
-
|
|
198
|
-
EXECUÇÃO PARALELA:
|
|
199
|
-
Todas as operações são executadas em AMBOS os providers (GitHub + Gitea) simultaneamente.
|
|
200
|
-
|
|
201
|
-
QUANDO USAR:
|
|
202
|
-
- Se git-workflow push falhar: use action='ensure'
|
|
203
|
-
- Para publicar release: use action='release-create'
|
|
204
|
-
- Para configurar tópicos: use action='topics-set'`;
|
|
205
|
-
|
|
206
|
-
async function handle(args) {
|
|
207
|
-
const validate = ajv.compile(inputSchema);
|
|
208
|
-
if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
209
|
-
const { projectPath, action } = args;
|
|
210
|
-
try {
|
|
211
|
-
validateProjectPath(projectPath);
|
|
212
|
-
if (action === "list") {
|
|
213
|
-
const remotes = await git.listRemotes(projectPath);
|
|
214
|
-
|
|
215
|
-
// Debug info: calculate what URLs should be
|
|
216
|
-
let repoName = getRepoNameFromPath(projectPath);
|
|
217
|
-
if (repoName === "GIT_MCP") repoName = "git-mcp";
|
|
218
|
-
const calculated = await pm.getRemoteUrls(repoName);
|
|
219
|
-
|
|
220
|
-
return asToolResult({
|
|
221
|
-
remotes,
|
|
222
|
-
configured: remotes.length > 0,
|
|
223
|
-
hasGithub: remotes.some(r => r.remote === "github"),
|
|
224
|
-
hasGitea: remotes.some(r => r.remote === "gitea"),
|
|
225
|
-
debug: {
|
|
226
|
-
repoName,
|
|
227
|
-
calculatedUrls: calculated,
|
|
228
|
-
env: {
|
|
229
|
-
hasGithubToken: !!pm.githubToken,
|
|
230
|
-
hasGiteaToken: !!pm.giteaToken,
|
|
231
|
-
giteaUrl: pm.giteaUrl
|
|
232
|
-
}
|
|
233
|
-
},
|
|
234
|
-
message: remotes.length === 0 ? "Nenhum remote configurado. Use action='ensure' para configurar." : undefined
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (action === "list-all") {
|
|
239
|
-
const repos = await pm.listAllRepos();
|
|
240
|
-
const summary = [];
|
|
241
|
-
if (repos.github?.length) summary.push(`${repos.github.length} GitHub repos`);
|
|
242
|
-
if (repos.gitea?.length) summary.push(`${repos.gitea.length} Gitea repos`);
|
|
243
|
-
|
|
244
|
-
return asToolResult({
|
|
245
|
-
success: true,
|
|
246
|
-
summary: summary.join(", "),
|
|
247
|
-
github: repos.github,
|
|
248
|
-
gitea: repos.gitea
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
if (action === "ensure") {
|
|
252
|
-
const repo = getRepoNameFromPath(projectPath);
|
|
253
|
-
const isPublic = args.isPublic === true; // Default: privado
|
|
254
|
-
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
|
|
255
|
-
const ghOwner = await pm.getGitHubOwner();
|
|
256
|
-
const geOwner = await pm.getGiteaOwner();
|
|
257
|
-
const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
|
|
258
|
-
const base = pm.giteaUrl?.replace(/\/$/, "") || "";
|
|
259
|
-
const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
|
|
260
|
-
await git.ensureRemotes(projectPath, { githubUrl, giteaUrl });
|
|
261
|
-
const remotes = await git.listRemotes(projectPath);
|
|
262
|
-
return asToolResult({
|
|
263
|
-
success: true,
|
|
264
|
-
isPrivate: !isPublic,
|
|
265
|
-
ensured,
|
|
266
|
-
remotes,
|
|
267
|
-
urls: { github: githubUrl, gitea: giteaUrl },
|
|
268
|
-
message: "Remotes configurados. Agora pode usar git-workflow push."
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
if (action === "repo-delete") {
|
|
272
|
-
const repo = getRepoNameFromPath(projectPath);
|
|
273
|
-
const out = await runBoth(pm, {
|
|
274
|
-
github: async (owner) => { await pm.github.rest.repos.delete({ owner, repo }); return { ok: true }; },
|
|
275
|
-
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 }; }
|
|
276
|
-
});
|
|
277
|
-
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), deleted: repo, providers: out, warning: "Repositório deletado permanentemente!" });
|
|
278
|
-
}
|
|
279
|
-
if (action === "release-create") {
|
|
280
|
-
const repo = getRepoNameFromPath(projectPath);
|
|
281
|
-
if (!args.tag) return asToolError("MISSING_PARAMETER", "tag é obrigatório para criar release", { parameter: "tag", example: "v1.0.0" });
|
|
282
|
-
const tag = args.tag;
|
|
283
|
-
const name = args.name || tag;
|
|
284
|
-
const body = args.body || "";
|
|
285
|
-
|
|
286
|
-
// Criar release primeiro
|
|
287
|
-
const out = await runBoth(pm, {
|
|
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 }; },
|
|
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 }; }
|
|
290
|
-
});
|
|
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
|
-
});
|
|
379
|
-
}
|
|
380
|
-
if (action === "topics-set") {
|
|
381
|
-
const repo = getRepoNameFromPath(projectPath);
|
|
382
|
-
const topics = Array.isArray(args.topics) ? args.topics : [];
|
|
383
|
-
if (topics.length === 0) return asToolError("MISSING_PARAMETER", "topics é obrigatório", { parameter: "topics", example: ["javascript", "nodejs"] });
|
|
384
|
-
const out = await runBoth(pm, {
|
|
385
|
-
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 }; },
|
|
386
|
-
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 }; }
|
|
387
|
-
});
|
|
388
|
-
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), topics, providers: out });
|
|
389
|
-
}
|
|
390
|
-
if (action === "milestone-create") {
|
|
391
|
-
const repo = getRepoNameFromPath(projectPath);
|
|
392
|
-
const title = args.title || args.name || "v1.0";
|
|
393
|
-
const out = await runBoth(pm, {
|
|
394
|
-
github: async (owner) => { const r = await pm.github.request("POST /repos/{owner}/{repo}/milestones", { owner, repo, title }); return { ok: true, id: r.data?.id }; },
|
|
395
|
-
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 }; }
|
|
396
|
-
});
|
|
397
|
-
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), title, providers: out });
|
|
398
|
-
}
|
|
399
|
-
if (action === "label-create") {
|
|
400
|
-
const repo = getRepoNameFromPath(projectPath);
|
|
401
|
-
const name = args.name || "bug";
|
|
402
|
-
const color = String(args.color || "ff0000").replace(/^#/, "");
|
|
403
|
-
const out = await runBoth(pm, {
|
|
404
|
-
github: async (owner) => { await pm.github.request("POST /repos/{owner}/{repo}/labels", { owner, repo, name, color }); return { ok: true }; },
|
|
405
|
-
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 }; }
|
|
406
|
-
});
|
|
407
|
-
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), label: { name, color }, providers: out });
|
|
408
|
-
}
|
|
409
|
-
if (action === "fork-create") {
|
|
410
|
-
const repo = getRepoNameFromPath(projectPath);
|
|
411
|
-
const out = await runBoth(pm, {
|
|
412
|
-
github: async (owner) => { const r = await pm.github.rest.repos.createFork({ owner, repo }); return { ok: true, full_name: r.data?.full_name }; },
|
|
413
|
-
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 }; }
|
|
414
|
-
});
|
|
415
|
-
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
|
|
416
|
-
}
|
|
417
|
-
if (action === "fork-list") {
|
|
418
|
-
const repo = getRepoNameFromPath(projectPath);
|
|
419
|
-
const out = await runBoth(pm, {
|
|
420
|
-
github: async (owner) => { const r = await pm.github.rest.repos.listForks({ owner, repo }); return { ok: true, count: r.data.length }; },
|
|
421
|
-
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 }; }
|
|
422
|
-
});
|
|
423
|
-
return asToolResult({ providers: out });
|
|
424
|
-
}
|
|
425
|
-
if (action === "star-set" || action === "star-unset") {
|
|
426
|
-
const repo = getRepoNameFromPath(projectPath);
|
|
427
|
-
const out = await runBoth(pm, {
|
|
428
|
-
github: async (owner) => {
|
|
429
|
-
if (action === "star-set") { await pm.github.request("PUT /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true }; }
|
|
430
|
-
await pm.github.request("DELETE /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true };
|
|
431
|
-
},
|
|
432
|
-
gitea: async (owner) => {
|
|
433
|
-
const base = pm.giteaUrl.replace(/\/$/, "");
|
|
434
|
-
if (action === "star-set") { await axios.put(`${base}/api/v1/user/starred/${owner}/${repo}`, {}, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
|
|
435
|
-
await axios.delete(`${base}/api/v1/user/starred/${owner}/${repo}`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true };
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), action, providers: out });
|
|
439
|
-
}
|
|
440
|
-
if (action === "subscription-set" || action === "subscription-unset") {
|
|
441
|
-
const repo = getRepoNameFromPath(projectPath);
|
|
442
|
-
const out = await runBoth(pm, {
|
|
443
|
-
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 }; },
|
|
444
|
-
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 }; }
|
|
445
|
-
});
|
|
446
|
-
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), action, providers: out });
|
|
447
|
-
}
|
|
448
|
-
if (action === "contents-create") {
|
|
449
|
-
const repo = getRepoNameFromPath(projectPath);
|
|
450
|
-
if (!args.path) return asToolError("MISSING_PARAMETER", "path é obrigatório", { parameter: "path" });
|
|
451
|
-
if (!args.content) return asToolError("MISSING_PARAMETER", "content é obrigatório", { parameter: "content" });
|
|
452
|
-
const filePath = args.path;
|
|
453
|
-
const message = args.message || `Add ${filePath}`;
|
|
454
|
-
const data = args.content;
|
|
455
|
-
const branch = args.branch || "main";
|
|
456
|
-
const b64 = Buffer.from(data, "utf8").toString("base64");
|
|
457
|
-
const out = await runBoth(pm, {
|
|
458
|
-
github: async (owner) => { await pm.github.request("PUT /repos/{owner}/{repo}/contents/{path}", { owner, repo, path: filePath, message, content: b64, branch }); return { ok: true }; },
|
|
459
|
-
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 }; }
|
|
460
|
-
});
|
|
461
|
-
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), path: filePath, branch, providers: out });
|
|
462
|
-
}
|
|
463
|
-
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
|
|
464
|
-
availableActions: ["list", "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"]
|
|
465
|
-
});
|
|
466
|
-
} catch (e) {
|
|
467
|
-
return errorToResponse(e);
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
return { name: "git-remote", description, inputSchema, handle };
|
|
472
|
-
}
|
|
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 } 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", "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 (GitHub + Gitea)
|
|
106
|
+
- ensure: Cria repos no GitHub/Gitea e configura remotes (USE ESTE se push falhar)
|
|
107
|
+
|
|
108
|
+
REPOSITÓRIO:
|
|
109
|
+
- repo-delete: Deleta repositório no GitHub E Gitea (⚠️ PERIGOSO)
|
|
110
|
+
- release-create: Cria release/versão no GitHub e Gitea
|
|
111
|
+
- topics-set: Define tópicos/tags do repositório
|
|
112
|
+
- milestone-create: Cria milestone para organização
|
|
113
|
+
- label-create: Cria label para issues
|
|
114
|
+
|
|
115
|
+
SOCIAL:
|
|
116
|
+
- star-set: Adiciona estrela ao repo
|
|
117
|
+
- star-unset: Remove estrela
|
|
118
|
+
- fork-create: Cria fork do repositório
|
|
119
|
+
- fork-list: Lista forks existentes
|
|
120
|
+
|
|
121
|
+
NOTIFICAÇÕES:
|
|
122
|
+
- subscription-set: Ativa notificações do repo
|
|
123
|
+
- subscription-unset: Desativa notificações
|
|
124
|
+
|
|
125
|
+
ARQUIVOS VIA API:
|
|
126
|
+
- contents-create: Cria arquivo diretamente via API (sem git local)`
|
|
127
|
+
},
|
|
128
|
+
tag: {
|
|
129
|
+
type: "string",
|
|
130
|
+
description: "Tag para release-create. Ex: 'v1.0.0'"
|
|
131
|
+
},
|
|
132
|
+
name: {
|
|
133
|
+
type: "string",
|
|
134
|
+
description: "Nome da release, label, etc."
|
|
135
|
+
},
|
|
136
|
+
body: {
|
|
137
|
+
type: "string",
|
|
138
|
+
description: "Descrição/corpo da release"
|
|
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
|
+
},
|
|
157
|
+
topics: {
|
|
158
|
+
type: "array",
|
|
159
|
+
items: { type: "string" },
|
|
160
|
+
description: "Lista de tópicos para topics-set. Ex: ['javascript', 'nodejs', 'mcp']"
|
|
161
|
+
},
|
|
162
|
+
path: {
|
|
163
|
+
type: "string",
|
|
164
|
+
description: "Caminho do arquivo para contents-create"
|
|
165
|
+
},
|
|
166
|
+
content: {
|
|
167
|
+
type: "string",
|
|
168
|
+
description: "Conteúdo do arquivo para contents-create"
|
|
169
|
+
},
|
|
170
|
+
branch: {
|
|
171
|
+
type: "string",
|
|
172
|
+
description: "Branch alvo para contents-create. Default: main"
|
|
173
|
+
},
|
|
174
|
+
color: {
|
|
175
|
+
type: "string",
|
|
176
|
+
description: "Cor do label em hex (sem #). Ex: 'ff0000' para vermelho"
|
|
177
|
+
},
|
|
178
|
+
title: {
|
|
179
|
+
type: "string",
|
|
180
|
+
description: "Título do milestone"
|
|
181
|
+
},
|
|
182
|
+
isPublic: {
|
|
183
|
+
type: "boolean",
|
|
184
|
+
description: "Se true, repositório será PÚBLICO. Default: false (privado). Aplica-se a action='ensure'"
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
required: ["projectPath", "action"],
|
|
188
|
+
additionalProperties: false
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const description = `Operações em repositórios remotos GitHub e Gitea.
|
|
192
|
+
|
|
193
|
+
AÇÕES MAIS USADAS:
|
|
194
|
+
- ensure: Configurar remotes e criar repos (ESSENCIAL antes de push)
|
|
195
|
+
- list: Ver remotes configurados
|
|
196
|
+
- release-create: Publicar nova versão
|
|
197
|
+
|
|
198
|
+
EXECUÇÃO PARALELA:
|
|
199
|
+
Todas as operações são executadas em AMBOS os providers (GitHub + Gitea) simultaneamente.
|
|
200
|
+
|
|
201
|
+
QUANDO USAR:
|
|
202
|
+
- Se git-workflow push falhar: use action='ensure'
|
|
203
|
+
- Para publicar release: use action='release-create'
|
|
204
|
+
- Para configurar tópicos: use action='topics-set'`;
|
|
205
|
+
|
|
206
|
+
async function handle(args) {
|
|
207
|
+
const validate = ajv.compile(inputSchema);
|
|
208
|
+
if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
|
|
209
|
+
const { projectPath, action } = args;
|
|
210
|
+
try {
|
|
211
|
+
validateProjectPath(projectPath);
|
|
212
|
+
if (action === "list") {
|
|
213
|
+
const remotes = await git.listRemotes(projectPath);
|
|
214
|
+
|
|
215
|
+
// Debug info: calculate what URLs should be
|
|
216
|
+
let repoName = getRepoNameFromPath(projectPath);
|
|
217
|
+
if (repoName === "GIT_MCP") repoName = "git-mcp";
|
|
218
|
+
const calculated = await pm.getRemoteUrls(repoName);
|
|
219
|
+
|
|
220
|
+
return asToolResult({
|
|
221
|
+
remotes,
|
|
222
|
+
configured: remotes.length > 0,
|
|
223
|
+
hasGithub: remotes.some(r => r.remote === "github"),
|
|
224
|
+
hasGitea: remotes.some(r => r.remote === "gitea"),
|
|
225
|
+
debug: {
|
|
226
|
+
repoName,
|
|
227
|
+
calculatedUrls: calculated,
|
|
228
|
+
env: {
|
|
229
|
+
hasGithubToken: !!pm.githubToken,
|
|
230
|
+
hasGiteaToken: !!pm.giteaToken,
|
|
231
|
+
giteaUrl: pm.giteaUrl
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
message: remotes.length === 0 ? "Nenhum remote configurado. Use action='ensure' para configurar." : undefined
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (action === "list-all") {
|
|
239
|
+
const repos = await pm.listAllRepos();
|
|
240
|
+
const summary = [];
|
|
241
|
+
if (repos.github?.length) summary.push(`${repos.github.length} GitHub repos`);
|
|
242
|
+
if (repos.gitea?.length) summary.push(`${repos.gitea.length} Gitea repos`);
|
|
243
|
+
|
|
244
|
+
return asToolResult({
|
|
245
|
+
success: true,
|
|
246
|
+
summary: summary.join(", "),
|
|
247
|
+
github: repos.github,
|
|
248
|
+
gitea: repos.gitea
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (action === "ensure") {
|
|
252
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
253
|
+
const isPublic = args.isPublic === true; // Default: privado
|
|
254
|
+
const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic });
|
|
255
|
+
const ghOwner = await pm.getGitHubOwner();
|
|
256
|
+
const geOwner = await pm.getGiteaOwner();
|
|
257
|
+
const githubUrl = ghOwner ? `https://github.com/${ghOwner}/${repo}.git` : "";
|
|
258
|
+
const base = pm.giteaUrl?.replace(/\/$/, "") || "";
|
|
259
|
+
const giteaUrl = geOwner && base ? `${base}/${geOwner}/${repo}.git` : "";
|
|
260
|
+
await git.ensureRemotes(projectPath, { githubUrl, giteaUrl });
|
|
261
|
+
const remotes = await git.listRemotes(projectPath);
|
|
262
|
+
return asToolResult({
|
|
263
|
+
success: true,
|
|
264
|
+
isPrivate: !isPublic,
|
|
265
|
+
ensured,
|
|
266
|
+
remotes,
|
|
267
|
+
urls: { github: githubUrl, gitea: giteaUrl },
|
|
268
|
+
message: "Remotes configurados. Agora pode usar git-workflow push."
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (action === "repo-delete") {
|
|
272
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
273
|
+
const out = await runBoth(pm, {
|
|
274
|
+
github: async (owner) => { await pm.github.rest.repos.delete({ owner, repo }); return { ok: true }; },
|
|
275
|
+
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 }; }
|
|
276
|
+
});
|
|
277
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), deleted: repo, providers: out, warning: "Repositório deletado permanentemente!" });
|
|
278
|
+
}
|
|
279
|
+
if (action === "release-create") {
|
|
280
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
281
|
+
if (!args.tag) return asToolError("MISSING_PARAMETER", "tag é obrigatório para criar release", { parameter: "tag", example: "v1.0.0" });
|
|
282
|
+
const tag = args.tag;
|
|
283
|
+
const name = args.name || tag;
|
|
284
|
+
const body = args.body || "";
|
|
285
|
+
|
|
286
|
+
// Criar release primeiro
|
|
287
|
+
const out = await runBoth(pm, {
|
|
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 }; },
|
|
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 }; }
|
|
290
|
+
});
|
|
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
|
+
});
|
|
379
|
+
}
|
|
380
|
+
if (action === "topics-set") {
|
|
381
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
382
|
+
const topics = Array.isArray(args.topics) ? args.topics : [];
|
|
383
|
+
if (topics.length === 0) return asToolError("MISSING_PARAMETER", "topics é obrigatório", { parameter: "topics", example: ["javascript", "nodejs"] });
|
|
384
|
+
const out = await runBoth(pm, {
|
|
385
|
+
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 }; },
|
|
386
|
+
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 }; }
|
|
387
|
+
});
|
|
388
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), topics, providers: out });
|
|
389
|
+
}
|
|
390
|
+
if (action === "milestone-create") {
|
|
391
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
392
|
+
const title = args.title || args.name || "v1.0";
|
|
393
|
+
const out = await runBoth(pm, {
|
|
394
|
+
github: async (owner) => { const r = await pm.github.request("POST /repos/{owner}/{repo}/milestones", { owner, repo, title }); return { ok: true, id: r.data?.id }; },
|
|
395
|
+
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 }; }
|
|
396
|
+
});
|
|
397
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), title, providers: out });
|
|
398
|
+
}
|
|
399
|
+
if (action === "label-create") {
|
|
400
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
401
|
+
const name = args.name || "bug";
|
|
402
|
+
const color = String(args.color || "ff0000").replace(/^#/, "");
|
|
403
|
+
const out = await runBoth(pm, {
|
|
404
|
+
github: async (owner) => { await pm.github.request("POST /repos/{owner}/{repo}/labels", { owner, repo, name, color }); return { ok: true }; },
|
|
405
|
+
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 }; }
|
|
406
|
+
});
|
|
407
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), label: { name, color }, providers: out });
|
|
408
|
+
}
|
|
409
|
+
if (action === "fork-create") {
|
|
410
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
411
|
+
const out = await runBoth(pm, {
|
|
412
|
+
github: async (owner) => { const r = await pm.github.rest.repos.createFork({ owner, repo }); return { ok: true, full_name: r.data?.full_name }; },
|
|
413
|
+
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 }; }
|
|
414
|
+
});
|
|
415
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), providers: out });
|
|
416
|
+
}
|
|
417
|
+
if (action === "fork-list") {
|
|
418
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
419
|
+
const out = await runBoth(pm, {
|
|
420
|
+
github: async (owner) => { const r = await pm.github.rest.repos.listForks({ owner, repo }); return { ok: true, count: r.data.length }; },
|
|
421
|
+
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 }; }
|
|
422
|
+
});
|
|
423
|
+
return asToolResult({ providers: out });
|
|
424
|
+
}
|
|
425
|
+
if (action === "star-set" || action === "star-unset") {
|
|
426
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
427
|
+
const out = await runBoth(pm, {
|
|
428
|
+
github: async (owner) => {
|
|
429
|
+
if (action === "star-set") { await pm.github.request("PUT /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true }; }
|
|
430
|
+
await pm.github.request("DELETE /user/starred/{owner}/{repo}", { owner, repo }); return { ok: true };
|
|
431
|
+
},
|
|
432
|
+
gitea: async (owner) => {
|
|
433
|
+
const base = pm.giteaUrl.replace(/\/$/, "");
|
|
434
|
+
if (action === "star-set") { await axios.put(`${base}/api/v1/user/starred/${owner}/${repo}`, {}, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true }; }
|
|
435
|
+
await axios.delete(`${base}/api/v1/user/starred/${owner}/${repo}`, { headers: { Authorization: `token ${pm.giteaToken}` } }); return { ok: true };
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), action, providers: out });
|
|
439
|
+
}
|
|
440
|
+
if (action === "subscription-set" || action === "subscription-unset") {
|
|
441
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
442
|
+
const out = await runBoth(pm, {
|
|
443
|
+
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 }; },
|
|
444
|
+
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 }; }
|
|
445
|
+
});
|
|
446
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), action, providers: out });
|
|
447
|
+
}
|
|
448
|
+
if (action === "contents-create") {
|
|
449
|
+
const repo = getRepoNameFromPath(projectPath);
|
|
450
|
+
if (!args.path) return asToolError("MISSING_PARAMETER", "path é obrigatório", { parameter: "path" });
|
|
451
|
+
if (!args.content) return asToolError("MISSING_PARAMETER", "content é obrigatório", { parameter: "content" });
|
|
452
|
+
const filePath = args.path;
|
|
453
|
+
const message = args.message || `Add ${filePath}`;
|
|
454
|
+
const data = args.content;
|
|
455
|
+
const branch = args.branch || "main";
|
|
456
|
+
const b64 = Buffer.from(data, "utf8").toString("base64");
|
|
457
|
+
const out = await runBoth(pm, {
|
|
458
|
+
github: async (owner) => { await pm.github.request("PUT /repos/{owner}/{repo}/contents/{path}", { owner, repo, path: filePath, message, content: b64, branch }); return { ok: true }; },
|
|
459
|
+
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 }; }
|
|
460
|
+
});
|
|
461
|
+
return asToolResult({ success: !!(out.github?.ok || out.gitea?.ok), path: filePath, branch, providers: out });
|
|
462
|
+
}
|
|
463
|
+
return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
|
|
464
|
+
availableActions: ["list", "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"]
|
|
465
|
+
});
|
|
466
|
+
} catch (e) {
|
|
467
|
+
return errorToResponse(e);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return { name: "git-remote", description, inputSchema, handle };
|
|
472
|
+
}
|