@andre.buzeli/git-mcp 16.1.3 → 16.1.4

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.
@@ -315,10 +315,28 @@ export class GitAdapter {
315
315
  suggestion: "Use git-workflow init para criar um repositório"
316
316
  });
317
317
  }
318
+ // Worktrees têm .git como arquivo, repos normais como diretório.
319
+ // Ambos são válidos e fs.existsSync retorna true para ambos.
318
320
  }
319
321
 
320
322
  /**
321
- * Verifica se o diretório é um repositório git
323
+ * Verifica se o diretório é um worktree (possui arquivo .git)
324
+ * @param {string} dir - Diretório para verificar
325
+ * @returns {boolean} - true se é um worktree
326
+ */
327
+ async isWorktree(dir) {
328
+ const gitPath = path.join(dir, ".git");
329
+ if (!fs.existsSync(gitPath)) return false;
330
+ try {
331
+ const stats = fs.statSync(gitPath);
332
+ return stats.isFile();
333
+ } catch {
334
+ return false;
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Verifica se o diretório é um repositório git (repo principal ou worktree)
322
340
  * @param {string} dir - Diretório para verificar
323
341
  * @returns {boolean} - true se é um repo git
324
342
  */
@@ -842,8 +860,12 @@ export class GitAdapter {
842
860
  try { await this._exec(dir, args); } catch { }
843
861
  }
844
862
 
845
- async listConfig(dir) {
846
- const out = await this._exec(dir, ["config", "--list"]);
863
+ async listConfig(dir, scope) {
864
+ const args = ["config", "--list"];
865
+ if (scope === "global") args.push("--global");
866
+ else if (scope === "system") args.push("--system");
867
+ else if (scope === "local") args.push("--local");
868
+ const out = await this._exec(dir, args);
847
869
  const items = {};
848
870
  out.split("\n").filter(Boolean).forEach(line => {
849
871
  const [k, ...v] = line.split("=");
@@ -959,16 +981,26 @@ export class GitAdapter {
959
981
  }
960
982
 
961
983
  // ============ DIFF/CLONE ============
962
- async diff(dir, options = {}) { return await this._exec(dir, ["diff"]); }
984
+ async diff(dir, options = {}) {
985
+ const args = ["diff"];
986
+ if (options.staged) {
987
+ args.push("--cached");
988
+ } else if (options.target) {
989
+ if (options.source) args.push(options.source);
990
+ args.push(options.target);
991
+ }
992
+ return await this._exec(dir, args);
993
+ }
963
994
  async diffCommits(dir, from, to) { return await this._exec(dir, ["diff", from, to]); }
964
995
 
965
- async clone(url, dir, options = {}) {
966
- const { branch, depth, singleBranch } = options;
996
+ async clone(dir, url, options = {}) {
997
+ const { name, branch, depth, singleBranch } = options;
967
998
  const args = ["clone"];
968
999
  if (branch) args.push("-b", branch);
969
1000
  if (depth) args.push("--depth", depth.toString());
970
1001
  if (singleBranch) args.push("--single-branch");
971
- args.push(url, ".");
1002
+ args.push(url);
1003
+ if (name) args.push(name);
972
1004
 
973
1005
  const header = this._getAuthHeader(url);
974
1006
  const cmdArgs = [];
@@ -998,9 +1030,10 @@ export class GitAdapter {
998
1030
  async removeFromGitignore(dir, patterns) {
999
1031
  const p = path.join(dir, ".gitignore");
1000
1032
  if (!fs.existsSync(p)) return;
1001
- let c = fs.readFileSync(p, "utf8");
1002
- patterns.forEach(pat => c = c.replace(pat, ""));
1003
- fs.writeFileSync(p, c);
1033
+ const lines = fs.readFileSync(p, "utf8").split("\n");
1034
+ const patternSet = new Set(patterns);
1035
+ const filtered = lines.filter(line => !patternSet.has(line.trim()));
1036
+ fs.writeFileSync(p, filtered.join("\n"));
1004
1037
  }
1005
1038
 
1006
1039
  async listRemotesRaw(dir) {
@@ -1029,121 +1062,128 @@ export class GitAdapter {
1029
1062
  return Array.from(map.values());
1030
1063
  }
1031
1064
 
1032
- // ============ WORKTREE ============
1033
-
1034
- async addWorktree(dir, branch, worktreePath) {
1035
- // Verifica se branch já existe
1036
- const branches = await this.listBranches(dir);
1037
- const branchExists = branches.includes(branch);
1038
-
1039
- const args = ["worktree", "add"];
1040
- // Se branch não existe, cria com -b. Se existe, usa checkout normal (sem flag)
1041
- if (!branchExists) {
1042
- args.push("-b", branch);
1043
- }
1044
- args.push(worktreePath, branch);
1045
-
1046
- await this._exec(dir, args);
1047
- }
1048
-
1049
- async listWorktrees(dir) {
1050
- try {
1051
- const out = await this._exec(dir, ["worktree", "list", "--porcelain"]);
1052
- const worktrees = [];
1053
- let current = {};
1054
-
1055
- const lines = out.split("\n");
1056
- for (const line of lines) {
1057
- if (!line.trim()) {
1058
- if (current.worktree) {
1059
- worktrees.push(current);
1060
- current = {};
1061
- }
1062
- continue;
1063
- }
1064
-
1065
- const [key, ...rest] = line.split(" ");
1066
- const value = rest.join(" ");
1067
-
1068
- if (key === "worktree") {
1069
- if (current.worktree) worktrees.push(current);
1070
- current = { worktree: value };
1071
- } else if (key === "branch") {
1072
- current.branch = value.replace("refs/heads/", "");
1073
- } else if (key === "HEAD") {
1074
- current.head = value;
1075
- }
1076
- }
1077
- if (current.worktree) worktrees.push(current);
1078
-
1079
- return worktrees.map(w => ({
1080
- path: w.worktree,
1081
- branch: w.branch,
1082
- head: w.head
1083
- }));
1084
- } catch (e) {
1085
- // Fallback para versão antiga do git se --porcelain falhar
1086
- const out = await this._exec(dir, ["worktree", "list"]);
1087
- return out.split("\n").filter(Boolean).map(line => {
1088
- const parts = line.split(/\s+/);
1089
- const path = parts.slice(0, -2).join(" "); // Path pode ter espaços
1090
- const head = parts[parts.length - 2];
1091
- const branchRaw = parts[parts.length - 1];
1092
- const branch = branchRaw.replace("[", "").replace("]", "");
1093
- return { path, head, branch };
1094
- });
1095
- }
1096
- }
1097
-
1098
- async removeWorktree(dir, worktreePath) {
1099
- await this._exec(dir, ["worktree", "remove", worktreePath, "--force"]);
1100
- }
1101
-
1102
- async pushRefspec(dir, remote, localBranch, remoteBranch, force = false) {
1103
- const remoteUrl = await this._exec(dir, ["remote", "get-url", remote]);
1104
- const header = this._getAuthHeader(remoteUrl);
1105
- const args = [];
1106
- if (header) args.push("-c", `http.extraHeader=${header}`);
1107
- args.push("push");
1108
- if (force) args.push("--force");
1109
- args.push(remote, `${localBranch}:${remoteBranch}`);
1110
- await this._exec(dir, args);
1111
- }
1112
-
1113
- async getWorktreeConfigs(dir) {
1114
- try {
1115
- const configPath = path.join(dir, ".git", "config");
1116
- if (!fs.existsSync(configPath)) return [];
1117
-
1118
- const content = fs.readFileSync(configPath, "utf8");
1119
- const results = [];
1120
- const sectionRegex = /\[worktree-branch "([^"]+)"\]([\s\S]*?)(?=\[|$)/g;
1121
-
1122
- let match;
1123
- while ((match = sectionRegex.exec(content)) !== null) {
1124
- const branchName = match[1];
1125
- const body = match[2];
1126
- const wtPath = body.match(/path\s*=\s*(.+)/)?.[1]?.trim();
1127
- const channel = body.match(/channel\s*=\s*(.+)/)?.[1]?.trim() || "production";
1128
- if (branchName && wtPath) {
1129
- results.push({ branch: branchName, path: wtPath, channel });
1130
- }
1131
- }
1132
- return results;
1133
- } catch (e) {
1134
- console.error("[GitAdapter] Error reading worktree configs:", e);
1135
- return [];
1136
- }
1137
- }
1138
-
1139
- async setWorktreeConfig(dir, branchName, config) {
1140
- // Normalizar path para usar forward slashes (compatível com git config em windows)
1141
- const normalizedPath = config.path.replace(/\\/g, "/");
1142
-
1143
- await this._exec(dir, ["config", `worktree-branch.${branchName}.path`, normalizedPath]);
1144
- await this._exec(dir, ["config", `worktree-branch.${branchName}.channel`, config.channel || "production"]);
1145
- }
1146
-
1065
+ // ============ WORKTREE ============
1066
+
1067
+ async addWorktree(dir, branch, worktreePath, force = false) {
1068
+ const branches = await this.listBranches(dir);
1069
+ const branchExists = branches.includes(branch);
1070
+
1071
+ const args = ["worktree", "add"];
1072
+ if (force) args.push("--force");
1073
+
1074
+ if (branchExists) {
1075
+ // Branch já existe: checkout dela no novo worktree
1076
+ args.push(worktreePath, branch);
1077
+ } else {
1078
+ // Branch nova: cria a partir do HEAD atual
1079
+ args.push("-b", branch, worktreePath);
1080
+ }
1081
+
1082
+ await this._exec(dir, args);
1083
+ }
1084
+
1085
+ async listWorktrees(dir) {
1086
+ try {
1087
+ const out = await this._exec(dir, ["worktree", "list", "--porcelain"]);
1088
+ const worktrees = [];
1089
+ let current = {};
1090
+
1091
+ const lines = out.split("\n");
1092
+ for (const line of lines) {
1093
+ if (!line.trim()) {
1094
+ if (current.worktree) {
1095
+ worktrees.push(current);
1096
+ current = {};
1097
+ }
1098
+ continue;
1099
+ }
1100
+
1101
+ const [key, ...rest] = line.split(" ");
1102
+ const value = rest.join(" ");
1103
+
1104
+ if (key === "worktree") {
1105
+ if (current.worktree) worktrees.push(current);
1106
+ current = { worktree: value };
1107
+ } else if (key === "branch") {
1108
+ current.branch = value.replace("refs/heads/", "");
1109
+ } else if (key === "HEAD") {
1110
+ current.head = value;
1111
+ }
1112
+ }
1113
+ if (current.worktree) worktrees.push(current);
1114
+
1115
+ return worktrees.map(w => ({
1116
+ path: w.worktree,
1117
+ branch: w.branch,
1118
+ head: w.head
1119
+ }));
1120
+ } catch (e) {
1121
+ // Fallback para versão antiga do git se --porcelain falhar
1122
+ const out = await this._exec(dir, ["worktree", "list"]);
1123
+ return out.split("\n").filter(Boolean).map(line => {
1124
+ const parts = line.split(/\s+/);
1125
+ const path = parts.slice(0, -2).join(" "); // Path pode ter espaços
1126
+ const head = parts[parts.length - 2];
1127
+ const branchRaw = parts[parts.length - 1];
1128
+ const branch = branchRaw.replace("[", "").replace("]", "");
1129
+ return { path, head, branch };
1130
+ });
1131
+ }
1132
+ }
1133
+
1134
+ async removeWorktree(dir, worktreePath) {
1135
+ await this._exec(dir, ["worktree", "remove", worktreePath, "--force"]);
1136
+ }
1137
+
1138
+ async pruneWorktrees(dir) {
1139
+ await this._exec(dir, ["worktree", "prune"]);
1140
+ }
1141
+
1142
+ async pushRefspec(dir, remote, localBranch, remoteBranch, force = false) {
1143
+ const remoteUrl = await this._exec(dir, ["remote", "get-url", remote]);
1144
+ const header = this._getAuthHeader(remoteUrl);
1145
+ const args = [];
1146
+ if (header) args.push("-c", `http.extraHeader=${header}`);
1147
+ args.push("push");
1148
+ if (force) args.push("--force");
1149
+ args.push(remote, `${localBranch}:${remoteBranch}`);
1150
+ await this._exec(dir, args);
1151
+ }
1152
+
1153
+ async getWorktreeConfigs(dir) {
1154
+ try {
1155
+ const configPath = path.join(dir, ".git", "config");
1156
+ if (!fs.existsSync(configPath)) return [];
1157
+
1158
+ const content = fs.readFileSync(configPath, "utf8");
1159
+ const results = [];
1160
+ const sectionRegex = /\[worktree-branch "([^"]+)"\]([\s\S]*?)(?=\[|$)/g;
1161
+
1162
+ let match;
1163
+ while ((match = sectionRegex.exec(content)) !== null) {
1164
+ const branchName = match[1];
1165
+ const body = match[2];
1166
+ const wtPath = body.match(/path\s*=\s*(.+)/)?.[1]?.trim();
1167
+ const channel = body.match(/channel\s*=\s*(.+)/)?.[1]?.trim() || "production";
1168
+ if (branchName && wtPath) {
1169
+ results.push({ branch: branchName, path: wtPath, channel });
1170
+ }
1171
+ }
1172
+ return results;
1173
+ } catch (e) {
1174
+ console.error("[GitAdapter] Error reading worktree configs:", e);
1175
+ return [];
1176
+ }
1177
+ }
1178
+
1179
+ async setWorktreeConfig(dir, branchName, config) {
1180
+ if (config.path !== undefined) {
1181
+ const normalizedPath = config.path.replace(/\\/g, "/");
1182
+ await this._exec(dir, ["config", `worktree-branch.${branchName}.path`, normalizedPath]);
1183
+ }
1184
+ await this._exec(dir, ["config", `worktree-branch.${branchName}.channel`, config.channel || "production"]);
1185
+ }
1186
+
1147
1187
  // ============ GIT LFS SUPPORT ============
1148
1188
 
1149
1189
  /**
@@ -19,7 +19,7 @@ const DEFAULT_OPTIONS = {
19
19
 
20
20
  function shouldRetry(error, options) {
21
21
  const msg = (error?.message || String(error)).toLowerCase();
22
- const code = error?.code?.toLowerCase() || "";
22
+ const code = String(error?.code ?? "").toLowerCase();
23
23
 
24
24
  return options.retryableErrors.some(e =>
25
25
  msg.includes(e.toLowerCase()) || code.includes(e.toLowerCase())
@@ -0,0 +1,189 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+
4
+ const PROTECTED_PATH_PREFIXES = [
5
+ 'C:\\Users',
6
+ 'C:/Users'
7
+ ];
8
+
9
+ /**
10
+ * Verifica se um caminho é protegido e não deve receber git init/worktree.
11
+ * Normaliza separadores e compara case-insensitive.
12
+ * @param {string} p - Caminho absoluto a verificar
13
+ * @returns {boolean} - True se for protegido
14
+ */
15
+ export function isProtectedPath(p) {
16
+ if (!p) return false;
17
+
18
+ const normalized = p.replace(/\\/g, '/').toLowerCase();
19
+
20
+ // Verifica se o caminho é temporário (permitido) APENAS em ambiente de teste
21
+ // Isso é importante para testes em ambientes onde o temp está dentro de Users
22
+ if (process.env.NODE_ENV === 'test') {
23
+ const tempDir = path.resolve(process.env.TEMP || process.env.TMP || '/tmp').replace(/\\/g, '/').toLowerCase();
24
+ if (normalized.startsWith(tempDir)) {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ for (const prefix of PROTECTED_PATH_PREFIXES) {
30
+ const normalizedPrefix = prefix.replace(/\\/g, '/').toLowerCase();
31
+
32
+ // Verifica igualdade exata ou se é subdiretório
33
+ if (normalized === normalizedPrefix || normalized.startsWith(normalizedPrefix + '/')) {
34
+ return true;
35
+ }
36
+ }
37
+
38
+ return false;
39
+ }
40
+
41
+ /**
42
+ * Busca ascendente por um diretório ou arquivo .git com limite de profundidade.
43
+ * @param {string} startPath - Caminho inicial absoluto
44
+ * @param {number} maxDepth - Número máximo de níveis para subir (default: 2)
45
+ * @returns {string|null} - Caminho do diretório contendo .git ou null se não encontrar
46
+ */
47
+ export function findGitRoot(startPath, maxDepth = 2) {
48
+ let current = path.resolve(startPath);
49
+ let depth = 0;
50
+
51
+ // Verifica o próprio startPath primeiro (depth 0)
52
+ if (fs.existsSync(path.join(current, '.git'))) {
53
+ return current;
54
+ }
55
+
56
+ // Sobe na árvore até maxDepth níveis ACIMA do startPath
57
+ while (depth < maxDepth) {
58
+ const parent = path.dirname(current);
59
+ if (parent === current) {
60
+ return null; // Chegou na raiz do FS
61
+ }
62
+
63
+ current = parent;
64
+ depth++;
65
+
66
+ if (fs.existsSync(path.join(current, '.git'))) {
67
+ return current;
68
+ }
69
+ }
70
+
71
+ return null;
72
+ }
73
+
74
+ /**
75
+ * Resolve o contexto de worktree para um dado caminho de projeto.
76
+ * Detecta se é worktree, repo principal, ou se precisa de auto-init.
77
+ * @param {string} projectPath - Caminho do projeto fornecido pelo agente
78
+ * @param {GitAdapter} gitAdapter - Instância do GitAdapter para executar comandos
79
+ * @returns {Promise<Object>} - WorktreeContext resolvido
80
+ */
81
+ export async function resolveWorktreeContext(projectPath, gitAdapter) {
82
+ const absolutePath = path.resolve(projectPath);
83
+
84
+ // Tenta encontrar .git no path ou acima (max 2 níveis)
85
+ // Se encontrar .git no próprio projectPath (depth 0), usa ele.
86
+ // Se encontrar acima, usa o pai como base.
87
+ let root = findGitRoot(absolutePath, 2);
88
+
89
+ if (!root) {
90
+ // Caso não encontre .git em até 2 níveis: Auto-init
91
+ if (isProtectedPath(absolutePath)) {
92
+ throw new Error(`PROTECTED_PATH: não é permitido criar repositório git em ${absolutePath}`);
93
+ }
94
+
95
+ // Executa git init
96
+ await gitAdapter._exec(absolutePath, ['init']);
97
+
98
+ // Após init, obtém a branch (provavelmente master/main)
99
+ const branch = await getBranchName(absolutePath, gitAdapter);
100
+
101
+ return {
102
+ repoRoot: absolutePath,
103
+ worktreePath: absolutePath,
104
+ branch: branch,
105
+ isWorktree: false,
106
+ gitCommonDir: path.join(absolutePath, '.git'),
107
+ autoInitialized: true
108
+ };
109
+ }
110
+
111
+ // Se encontrou raiz (pode ser o próprio projectPath ou um pai)
112
+ const gitEntry = path.join(root, '.git');
113
+ const stats = fs.statSync(gitEntry);
114
+
115
+ if (stats.isDirectory()) {
116
+ const branch = await getBranchName(root, gitAdapter);
117
+
118
+ // Req 9.2: valida que gitCommonDir contém objects/
119
+ if (!fs.existsSync(path.join(gitEntry, 'objects'))) {
120
+ throw new Error(`WORKTREE_CORRUPT: Diretório objects não encontrado em ${gitEntry}`);
121
+ }
122
+
123
+ return {
124
+ repoRoot: root,
125
+ worktreePath: root,
126
+ branch: branch,
127
+ isWorktree: false,
128
+ gitCommonDir: gitEntry
129
+ };
130
+ } else if (stats.isFile()) {
131
+ // Worktree
132
+ const content = fs.readFileSync(gitEntry, 'utf8');
133
+ const match = content.match(/^gitdir:\s*(.+)$/m);
134
+
135
+ if (!match) {
136
+ throw new Error('WORKTREE_CORRUPT: Arquivo .git inválido ou sem formato gitdir');
137
+ }
138
+
139
+ const gitdirRelative = match[1].trim();
140
+ const gitdirAbsolute = path.resolve(root, gitdirRelative);
141
+
142
+ if (!fs.existsSync(gitdirAbsolute)) {
143
+ throw new Error(`WORKTREE_STALE: gitdir ${gitdirAbsolute} não encontrado. Execute 'git worktree prune' no repo principal.`);
144
+ }
145
+
146
+ // Tenta achar commondir
147
+ const commondirFile = path.join(gitdirAbsolute, 'commondir');
148
+ let gitCommonDir;
149
+
150
+ if (fs.existsSync(commondirFile)) {
151
+ const commondirRelative = fs.readFileSync(commondirFile, 'utf8').trim();
152
+ gitCommonDir = path.resolve(gitdirAbsolute, commondirRelative);
153
+ } else {
154
+ gitCommonDir = gitdirAbsolute;
155
+ }
156
+
157
+ // Valida objects
158
+ if (!fs.existsSync(path.join(gitCommonDir, 'objects'))) {
159
+ throw new Error(`WORKTREE_CORRUPT: Diretório objects não encontrado em ${gitCommonDir}`);
160
+ }
161
+
162
+ const repoRoot = path.dirname(gitCommonDir);
163
+ const branch = await getBranchName(root, gitAdapter);
164
+
165
+ return {
166
+ repoRoot: repoRoot,
167
+ worktreePath: root,
168
+ branch: branch,
169
+ isWorktree: true,
170
+ gitCommonDir: gitCommonDir
171
+ };
172
+ }
173
+
174
+ throw new Error('NOT_A_GIT_REPO: .git entry is neither file nor directory');
175
+ }
176
+
177
+ /**
178
+ * Helper para obter o nome da branch atual
179
+ */
180
+ async function getBranchName(cwd, gitAdapter) {
181
+ try {
182
+ const result = await gitAdapter._exec(cwd, ['rev-parse', '--abbrev-ref', 'HEAD']);
183
+ return result.trim();
184
+ } catch (error) {
185
+ // Em repo recém inicializado sem commits, pode falhar ou retornar HEAD.
186
+ // Mas git init cria branch default.
187
+ return 'HEAD';
188
+ }
189
+ }