@andrebuzeli/git-mcp 12.0.0 → 13.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +4 -4
- package/src/tools/git-branches.js +21 -7
- package/src/tools/git-config.js +8 -9
- package/src/tools/git-files.js +18 -8
- package/src/tools/git-history.js +22 -5
- package/src/tools/git-ignore.js +19 -8
- package/src/tools/git-issues.js +3 -3
- package/src/tools/git-pulls.js +3 -3
- package/src/tools/git-remote.js +5 -3
- package/src/tools/git-reset.js +20 -5
- package/src/tools/git-stash.js +31 -10
- package/src/tools/git-sync.js +21 -5
- package/src/tools/git-tags.js +34 -13
- package/src/tools/git-workflow.js +10 -6
- package/src/utils/errors.js +276 -1
- package/src/utils/gitAdapter.js +392 -9
- package/src/utils/retry.js +58 -9
package/src/utils/gitAdapter.js
CHANGED
|
@@ -2,8 +2,9 @@ import * as git from "isomorphic-git";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import http from "isomorphic-git/http/node";
|
|
5
|
-
import { MCPError } from "./errors.js";
|
|
5
|
+
import { MCPError, createError, mapExternalError } from "./errors.js";
|
|
6
6
|
import { getProvidersEnv } from "./repoHelpers.js";
|
|
7
|
+
import { withRetry } from "./retry.js";
|
|
7
8
|
|
|
8
9
|
export class GitAdapter {
|
|
9
10
|
constructor(providerManager) {
|
|
@@ -90,27 +91,409 @@ export class GitAdapter {
|
|
|
90
91
|
getAuth(remoteUrl) {
|
|
91
92
|
const { githubToken, giteaToken } = getProvidersEnv();
|
|
92
93
|
if (remoteUrl.includes("github.com")) {
|
|
93
|
-
if (!githubToken) throw
|
|
94
|
+
if (!githubToken) throw createError("AUTH_GITHUB_INVALID", { message: "GITHUB_TOKEN não configurado" });
|
|
94
95
|
return () => ({ username: githubToken, password: "x-oauth-basic" });
|
|
95
96
|
}
|
|
96
97
|
if (giteaToken) return () => ({ username: "git", password: giteaToken });
|
|
97
|
-
throw
|
|
98
|
+
throw createError("AUTH_GITEA_INVALID", { message: "GITEA_TOKEN não configurado" });
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
async pushOne(dir, remote, branch) {
|
|
101
|
+
async pushOne(dir, remote, branch, force = false) {
|
|
101
102
|
const remotes = await git.listRemotes({ fs, dir });
|
|
102
103
|
const info = remotes.find(r => r.remote === remote);
|
|
103
|
-
if (!info)
|
|
104
|
+
if (!info) {
|
|
105
|
+
const available = remotes.map(r => r.remote).join(", ");
|
|
106
|
+
throw createError("REMOTE_NOT_FOUND", {
|
|
107
|
+
message: `Remote '${remote}' não encontrado`,
|
|
108
|
+
remote,
|
|
109
|
+
availableRemotes: available || "nenhum"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
104
112
|
const onAuth = this.getAuth(info.url);
|
|
105
113
|
const ref = branch.startsWith("refs/") ? branch : `refs/heads/${branch}`;
|
|
106
|
-
|
|
114
|
+
try {
|
|
115
|
+
await withRetry(() => git.push({ fs, http, dir, remote, ref, remoteRef: ref, onAuth, force }));
|
|
116
|
+
} catch (e) {
|
|
117
|
+
if (e.message?.includes("non-fast-forward") || e.message?.includes("rejected")) {
|
|
118
|
+
throw createError("PUSH_REJECTED", { message: e.message, remote, branch });
|
|
119
|
+
}
|
|
120
|
+
throw mapExternalError(e, { type: "push", remote, branch });
|
|
121
|
+
}
|
|
107
122
|
}
|
|
108
123
|
|
|
109
|
-
async pushParallel(dir, branch) {
|
|
124
|
+
async pushParallel(dir, branch, force = false) {
|
|
110
125
|
const remotes = await git.listRemotes({ fs, dir });
|
|
111
126
|
const targets = remotes.filter(r => ["github", "gitea"].includes(r.remote));
|
|
112
|
-
if (targets.length === 0)
|
|
113
|
-
|
|
127
|
+
if (targets.length === 0) {
|
|
128
|
+
throw createError("REMOTE_NOT_FOUND", {
|
|
129
|
+
message: "Nenhum remote github/gitea configurado",
|
|
130
|
+
availableRemotes: remotes.map(r => r.remote).join(", ") || "nenhum",
|
|
131
|
+
suggestion: "Use action='ensure-remotes' para configurar os remotes"
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const results = await Promise.allSettled(targets.map(t => this.pushOne(dir, t.remote, branch, force)));
|
|
135
|
+
const errors = results.filter(r => r.status === "rejected");
|
|
136
|
+
if (errors.length === targets.length) {
|
|
137
|
+
throw createError("PUSH_REJECTED", {
|
|
138
|
+
message: "Push falhou para todos os remotes",
|
|
139
|
+
errors: errors.map(e => e.reason?.message || String(e.reason))
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
pushed: targets.filter((t, i) => results[i].status === "fulfilled").map(t => t.remote),
|
|
144
|
+
failed: errors.map((e, i) => ({ remote: targets[i]?.remote, error: e.reason?.message }))
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============ BRANCHES ============
|
|
149
|
+
async listBranches(dir, remote = false) {
|
|
150
|
+
if (remote) {
|
|
151
|
+
const remotes = await git.listRemotes({ fs, dir });
|
|
152
|
+
const branches = [];
|
|
153
|
+
for (const r of remotes) {
|
|
154
|
+
try {
|
|
155
|
+
const refs = await git.listServerRefs({ http, url: r.url, prefix: "refs/heads/", onAuth: this.getAuth(r.url) });
|
|
156
|
+
for (const ref of refs) branches.push(`${r.remote}/${ref.ref.replace("refs/heads/", "")}`);
|
|
157
|
+
} catch { /* ignore */ }
|
|
158
|
+
}
|
|
159
|
+
return branches;
|
|
160
|
+
}
|
|
161
|
+
return await git.listBranches({ fs, dir });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async createBranch(dir, name, ref = "HEAD") {
|
|
165
|
+
const oid = await git.resolveRef({ fs, dir, ref });
|
|
166
|
+
await git.branch({ fs, dir, ref: name, checkout: false });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async deleteBranch(dir, name, force = false) {
|
|
170
|
+
await git.deleteBranch({ fs, dir, ref: name });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async renameBranch(dir, oldName, newName) {
|
|
174
|
+
await git.renameBranch({ fs, dir, oldref: oldName, ref: newName });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async checkout(dir, ref) {
|
|
178
|
+
await git.checkout({ fs, dir, ref, force: true });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ============ TAGS ============
|
|
182
|
+
async listTags(dir) {
|
|
183
|
+
return await git.listTags({ fs, dir });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async createTag(dir, tag, ref = "HEAD", message = "") {
|
|
187
|
+
const oid = await git.resolveRef({ fs, dir, ref });
|
|
188
|
+
if (message) {
|
|
189
|
+
const author = await this.getAuthor(dir);
|
|
190
|
+
await git.tag({ fs, dir, ref: tag, object: oid, tagger: { name: author.name, email: author.email }, message });
|
|
191
|
+
} else {
|
|
192
|
+
await git.tag({ fs, dir, ref: tag, object: oid });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async deleteTag(dir, tag) {
|
|
197
|
+
await git.deleteTag({ fs, dir, ref: tag });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async pushTag(dir, remote, tag) {
|
|
201
|
+
const remotes = await git.listRemotes({ fs, dir });
|
|
202
|
+
const info = remotes.find(r => r.remote === remote);
|
|
203
|
+
if (!info) throw new MCPError("REMOTE", `Remote '${remote}' não encontrado`);
|
|
204
|
+
const onAuth = this.getAuth(info.url);
|
|
205
|
+
await git.push({ fs, http, dir, remote, ref: `refs/tags/${tag}`, remoteRef: `refs/tags/${tag}`, onAuth });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ============ STASH (simulated via JSON file) ============
|
|
209
|
+
_getStashFile(dir) {
|
|
210
|
+
return path.join(dir, ".git", "stash.json");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async listStash(dir) {
|
|
214
|
+
const stashFile = this._getStashFile(dir);
|
|
215
|
+
if (!fs.existsSync(stashFile)) return [];
|
|
216
|
+
try {
|
|
217
|
+
return JSON.parse(fs.readFileSync(stashFile, "utf8"));
|
|
218
|
+
} catch { return []; }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async saveStash(dir, message = "WIP", includeUntracked = false) {
|
|
222
|
+
const st = await this.status(dir);
|
|
223
|
+
if (st.isClean && !includeUntracked) {
|
|
224
|
+
throw createError("NOTHING_TO_STASH", {
|
|
225
|
+
message: "Working tree limpa, nada para stash",
|
|
226
|
+
status: st
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Salva arquivos modificados/adicionados
|
|
231
|
+
const filesToSave = [...st.not_added, ...st.modified, ...st.created];
|
|
232
|
+
const stashData = { message, timestamp: Date.now(), files: {} };
|
|
233
|
+
|
|
234
|
+
for (const file of filesToSave) {
|
|
235
|
+
const fullPath = path.join(dir, file);
|
|
236
|
+
if (fs.existsSync(fullPath)) {
|
|
237
|
+
stashData.files[file] = fs.readFileSync(fullPath, "utf8");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Carrega stashes existentes
|
|
242
|
+
const stashes = await this.listStash(dir);
|
|
243
|
+
stashes.unshift(stashData);
|
|
244
|
+
|
|
245
|
+
// Salva lista de stashes
|
|
246
|
+
fs.writeFileSync(this._getStashFile(dir), JSON.stringify(stashes, null, 2));
|
|
247
|
+
|
|
248
|
+
// Restaura arquivos para estado HEAD mantendo a branch atual
|
|
249
|
+
const currentBranch = await this.getCurrentBranch(dir);
|
|
250
|
+
if (currentBranch && currentBranch !== "HEAD") {
|
|
251
|
+
await git.checkout({ fs, dir, ref: currentBranch, force: true });
|
|
252
|
+
} else {
|
|
253
|
+
// Se estiver em detached HEAD, usa o SHA atual
|
|
254
|
+
const oid = await git.resolveRef({ fs, dir, ref: "HEAD" });
|
|
255
|
+
await git.checkout({ fs, dir, ref: oid, force: true });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async applyStash(dir, ref = "stash@{0}") {
|
|
260
|
+
const stashes = await this.listStash(dir);
|
|
261
|
+
const idx = Number(ref.match(/\{(\d+)\}/)?.[1] || 0);
|
|
262
|
+
const stash = stashes[idx];
|
|
263
|
+
if (!stash) {
|
|
264
|
+
throw createError("STASH_NOT_FOUND", {
|
|
265
|
+
message: `Stash '${ref}' não encontrado`,
|
|
266
|
+
requestedIndex: idx,
|
|
267
|
+
availableStashes: stashes.length,
|
|
268
|
+
stashList: stashes.map((s, i) => `stash@{${i}}: ${s.message}`).slice(0, 5)
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Restaura arquivos do stash
|
|
273
|
+
for (const [file, content] of Object.entries(stash.files)) {
|
|
274
|
+
const fullPath = path.join(dir, file);
|
|
275
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
276
|
+
fs.writeFileSync(fullPath, content);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async popStash(dir, ref = "stash@{0}") {
|
|
281
|
+
await this.applyStash(dir, ref);
|
|
282
|
+
await this.dropStash(dir, ref);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async dropStash(dir, ref = "stash@{0}") {
|
|
286
|
+
const stashes = await this.listStash(dir);
|
|
287
|
+
const idx = Number(ref.match(/\{(\d+)\}/)?.[1] || 0);
|
|
288
|
+
stashes.splice(idx, 1);
|
|
289
|
+
|
|
290
|
+
if (stashes.length === 0) {
|
|
291
|
+
const stashFile = this._getStashFile(dir);
|
|
292
|
+
if (fs.existsSync(stashFile)) fs.unlinkSync(stashFile);
|
|
293
|
+
} else {
|
|
294
|
+
fs.writeFileSync(this._getStashFile(dir), JSON.stringify(stashes, null, 2));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async clearStash(dir) {
|
|
299
|
+
const stashFile = this._getStashFile(dir);
|
|
300
|
+
if (fs.existsSync(stashFile)) fs.unlinkSync(stashFile);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ============ CONFIG ============
|
|
304
|
+
async getConfig(dir, key, scope = "local") {
|
|
305
|
+
return await git.getConfig({ fs, dir, path: key }).catch(() => undefined);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async setConfig(dir, key, value, scope = "local") {
|
|
309
|
+
await git.setConfig({ fs, dir, path: key, value });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async unsetConfig(dir, key, scope = "local") {
|
|
313
|
+
// isomorphic-git não tem unset, então setamos para undefined
|
|
314
|
+
const configPath = path.join(dir, ".git", "config");
|
|
315
|
+
if (!fs.existsSync(configPath)) return;
|
|
316
|
+
let content = fs.readFileSync(configPath, "utf8");
|
|
317
|
+
const regex = new RegExp(`^\\s*${key.split(".").pop()}\\s*=.*$`, "gm");
|
|
318
|
+
content = content.replace(regex, "");
|
|
319
|
+
fs.writeFileSync(configPath, content);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async listConfig(dir, scope = "local") {
|
|
323
|
+
const configPath = path.join(dir, ".git", "config");
|
|
324
|
+
const items = {};
|
|
325
|
+
if (!fs.existsSync(configPath)) return items;
|
|
326
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
327
|
+
let section = "";
|
|
328
|
+
for (const line of content.split("\n")) {
|
|
329
|
+
const secMatch = line.match(/^\[([^\]]+)\]$/);
|
|
330
|
+
if (secMatch) { section = secMatch[1].replace(/\s+"/g, ".").replace(/"/g, ""); continue; }
|
|
331
|
+
const kvMatch = line.match(/^\s*(\w+)\s*=\s*(.*)$/);
|
|
332
|
+
if (kvMatch && section) items[`${section}.${kvMatch[1]}`] = kvMatch[2].trim();
|
|
333
|
+
}
|
|
334
|
+
return items;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ============ FILES ============
|
|
338
|
+
async listFiles(dir, ref = "HEAD") {
|
|
339
|
+
const oid = await git.resolveRef({ fs, dir, ref });
|
|
340
|
+
const { tree } = await git.readTree({ fs, dir, oid });
|
|
341
|
+
const files = [];
|
|
342
|
+
const walk = async (treePath, entries) => {
|
|
343
|
+
for (const entry of entries) {
|
|
344
|
+
const fullPath = treePath ? `${treePath}/${entry.path}` : entry.path;
|
|
345
|
+
if (entry.type === "blob") files.push(fullPath);
|
|
346
|
+
else if (entry.type === "tree") {
|
|
347
|
+
const { tree: subTree } = await git.readTree({ fs, dir, oid: entry.oid });
|
|
348
|
+
await walk(fullPath, subTree);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
await walk("", tree);
|
|
353
|
+
return files;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async readFile(dir, filepath, ref = "HEAD") {
|
|
357
|
+
const oid = await git.resolveRef({ fs, dir, ref });
|
|
358
|
+
const { blob } = await git.readBlob({ fs, dir, oid, filepath });
|
|
359
|
+
return new TextDecoder().decode(blob);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ============ HISTORY ============
|
|
363
|
+
async log(dir, { ref = "HEAD", maxCount = 50 } = {}) {
|
|
364
|
+
const commits = await git.log({ fs, dir, ref, depth: maxCount });
|
|
365
|
+
return commits.map(c => ({
|
|
366
|
+
sha: c.oid,
|
|
367
|
+
message: c.commit.message,
|
|
368
|
+
author: c.commit.author,
|
|
369
|
+
date: new Date(c.commit.author.timestamp * 1000).toISOString()
|
|
370
|
+
}));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ============ SYNC (FETCH/PULL) ============
|
|
374
|
+
async fetch(dir, remote, branch) {
|
|
375
|
+
const remotes = await git.listRemotes({ fs, dir });
|
|
376
|
+
const info = remotes.find(r => r.remote === remote);
|
|
377
|
+
if (!info) {
|
|
378
|
+
throw createError("REMOTE_NOT_FOUND", {
|
|
379
|
+
message: `Remote '${remote}' não encontrado`,
|
|
380
|
+
availableRemotes: remotes.map(r => r.remote)
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
const onAuth = this.getAuth(info.url);
|
|
384
|
+
try {
|
|
385
|
+
await withRetry(() => git.fetch({ fs, http, dir, remote, ref: branch, singleBranch: true, onAuth }));
|
|
386
|
+
} catch (e) {
|
|
387
|
+
throw mapExternalError(e, { type: "fetch", remote, branch, provider: info.url.includes("github") ? "github" : "gitea" });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async pull(dir, remote, branch) {
|
|
392
|
+
const remotes = await git.listRemotes({ fs, dir });
|
|
393
|
+
const info = remotes.find(r => r.remote === remote);
|
|
394
|
+
if (!info) {
|
|
395
|
+
throw createError("REMOTE_NOT_FOUND", {
|
|
396
|
+
message: `Remote '${remote}' não encontrado`,
|
|
397
|
+
availableRemotes: remotes.map(r => r.remote)
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
const onAuth = this.getAuth(info.url);
|
|
401
|
+
const author = await this.getAuthor(dir);
|
|
402
|
+
try {
|
|
403
|
+
await withRetry(async () => git.pull({ fs, http, dir, remote, ref: branch, singleBranch: true, onAuth, author }));
|
|
404
|
+
} catch (e) {
|
|
405
|
+
if (e.message?.includes("conflict")) {
|
|
406
|
+
throw createError("MERGE_CONFLICT", { message: e.message, remote, branch });
|
|
407
|
+
}
|
|
408
|
+
throw mapExternalError(e, { type: "pull", remote, branch, provider: info.url.includes("github") ? "github" : "gitea" });
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ============ RESET ============
|
|
413
|
+
async _resolveRef(dir, ref) {
|
|
414
|
+
// Resolve refs como HEAD~1, HEAD~2, etc.
|
|
415
|
+
const match = ref.match(/^(HEAD|[a-zA-Z0-9_\-\/]+)~(\d+)$/);
|
|
416
|
+
if (match) {
|
|
417
|
+
const baseRef = match[1];
|
|
418
|
+
const steps = parseInt(match[2], 10);
|
|
419
|
+
const commits = await git.log({ fs, dir, ref: baseRef, depth: steps + 1 });
|
|
420
|
+
if (commits.length > steps) return commits[steps].oid;
|
|
421
|
+
throw createError("INSUFFICIENT_HISTORY", {
|
|
422
|
+
message: `Não foi possível resolver ${ref}`,
|
|
423
|
+
requestedSteps: steps,
|
|
424
|
+
availableCommits: commits.length,
|
|
425
|
+
suggestion: `Histórico tem apenas ${commits.length} commits. Use HEAD~${commits.length - 1} no máximo.`
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
// Refs normais
|
|
429
|
+
try {
|
|
430
|
+
return await git.resolveRef({ fs, dir, ref });
|
|
431
|
+
} catch (e) {
|
|
432
|
+
const branches = await git.listBranches({ fs, dir }).catch(() => []);
|
|
433
|
+
const tags = await git.listTags({ fs, dir }).catch(() => []);
|
|
434
|
+
throw createError("REF_NOT_FOUND", {
|
|
435
|
+
message: `Ref '${ref}' não encontrada`,
|
|
436
|
+
ref,
|
|
437
|
+
availableBranches: branches.slice(0, 10),
|
|
438
|
+
availableTags: tags.slice(0, 10)
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async resetSoft(dir, ref) {
|
|
444
|
+
const oid = await this._resolveRef(dir, ref);
|
|
445
|
+
const headPath = path.join(dir, ".git", "HEAD");
|
|
446
|
+
const headContent = fs.readFileSync(headPath, "utf8").trim();
|
|
447
|
+
if (headContent.startsWith("ref:")) {
|
|
448
|
+
const branchRef = headContent.replace("ref: ", "");
|
|
449
|
+
const branchPath = path.join(dir, ".git", branchRef);
|
|
450
|
+
fs.writeFileSync(branchPath, oid + "\n");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async resetMixed(dir, ref) {
|
|
455
|
+
await this.resetSoft(dir, ref);
|
|
456
|
+
// Reset index to match ref
|
|
457
|
+
const oid = await this._resolveRef(dir, ref);
|
|
458
|
+
await git.checkout({ fs, dir, ref: oid, noUpdateHead: true });
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async resetHard(dir, ref) {
|
|
462
|
+
const oid = await this._resolveRef(dir, ref);
|
|
463
|
+
await git.checkout({ fs, dir, ref: oid, force: true });
|
|
464
|
+
await this.resetSoft(dir, ref);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ============ GITIGNORE ============
|
|
468
|
+
async listGitignore(dir) {
|
|
469
|
+
const ignorePath = path.join(dir, ".gitignore");
|
|
470
|
+
if (!fs.existsSync(ignorePath)) return [];
|
|
471
|
+
return fs.readFileSync(ignorePath, "utf8").split("\n").filter(l => l.trim() && !l.startsWith("#"));
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async createGitignore(dir, patterns = []) {
|
|
475
|
+
const ignorePath = path.join(dir, ".gitignore");
|
|
476
|
+
fs.writeFileSync(ignorePath, patterns.join("\n") + "\n");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async addToGitignore(dir, patterns = []) {
|
|
480
|
+
const ignorePath = path.join(dir, ".gitignore");
|
|
481
|
+
const existing = fs.existsSync(ignorePath) ? fs.readFileSync(ignorePath, "utf8") : "";
|
|
482
|
+
const newContent = existing.trimEnd() + "\n" + patterns.join("\n") + "\n";
|
|
483
|
+
fs.writeFileSync(ignorePath, newContent);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async removeFromGitignore(dir, patterns = []) {
|
|
487
|
+
const ignorePath = path.join(dir, ".gitignore");
|
|
488
|
+
if (!fs.existsSync(ignorePath)) return;
|
|
489
|
+
let lines = fs.readFileSync(ignorePath, "utf8").split("\n");
|
|
490
|
+
lines = lines.filter(l => !patterns.includes(l.trim()));
|
|
491
|
+
fs.writeFileSync(ignorePath, lines.join("\n"));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ============ REMOTES ============
|
|
495
|
+
async listRemotes(dir) {
|
|
496
|
+
return await git.listRemotes({ fs, dir });
|
|
114
497
|
}
|
|
115
498
|
}
|
|
116
499
|
|
package/src/utils/retry.js
CHANGED
|
@@ -1,12 +1,61 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
// Sistema de Retry com Backoff Exponencial
|
|
2
|
+
|
|
3
|
+
const DEFAULT_OPTIONS = {
|
|
4
|
+
maxRetries: 3,
|
|
5
|
+
initialDelay: 1000,
|
|
6
|
+
maxDelay: 10000,
|
|
7
|
+
backoffFactor: 2,
|
|
8
|
+
retryableErrors: ["ETIMEDOUT", "ECONNRESET", "ENOTFOUND", "ECONNREFUSED", "timeout", "rate limit", "429", "503", "502"]
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function shouldRetry(error, options) {
|
|
12
|
+
const msg = (error?.message || String(error)).toLowerCase();
|
|
13
|
+
const code = error?.code?.toLowerCase() || "";
|
|
14
|
+
|
|
15
|
+
return options.retryableErrors.some(e =>
|
|
16
|
+
msg.includes(e.toLowerCase()) || code.includes(e.toLowerCase())
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sleep(ms) {
|
|
21
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function withRetry(fn, options = {}) {
|
|
25
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
26
|
+
let lastError;
|
|
27
|
+
let delay = opts.initialDelay;
|
|
28
|
+
|
|
29
|
+
for (let attempt = 1; attempt <= opts.maxRetries; attempt++) {
|
|
30
|
+
try {
|
|
31
|
+
return await fn();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
lastError = error;
|
|
34
|
+
|
|
35
|
+
if (attempt === opts.maxRetries || !shouldRetry(error, opts)) {
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Log retry (silencioso)
|
|
40
|
+
console.error(`[Retry] Attempt ${attempt}/${opts.maxRetries} failed, retrying in ${delay}ms...`);
|
|
41
|
+
|
|
42
|
+
await sleep(delay);
|
|
43
|
+
delay = Math.min(delay * opts.backoffFactor, opts.maxDelay);
|
|
44
|
+
}
|
|
9
45
|
}
|
|
10
|
-
|
|
46
|
+
|
|
47
|
+
throw lastError;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Wrapper para axios com retry
|
|
51
|
+
export async function axiosWithRetry(axiosInstance, config, options = {}) {
|
|
52
|
+
return withRetry(() => axiosInstance(config), options);
|
|
11
53
|
}
|
|
12
54
|
|
|
55
|
+
// Wrapper para operações git com retry
|
|
56
|
+
export async function gitWithRetry(fn, options = {}) {
|
|
57
|
+
return withRetry(fn, {
|
|
58
|
+
...options,
|
|
59
|
+
retryableErrors: [...DEFAULT_OPTIONS.retryableErrors, "ENOENT", "lock"]
|
|
60
|
+
});
|
|
61
|
+
}
|