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