@andre.buzeli/git-mcp 16.1.3 → 16.1.6

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.
@@ -1,5 +1,5 @@
1
1
  import Ajv from "ajv";
2
- import { asToolError, asToolResult, errorToResponse, createError } from "../utils/errors.js";
2
+ import { asToolError, asToolResult, errorToResponse, createError, handleEmptyCall } from "../utils/errors.js";
3
3
  import { validateProjectPath } from "../utils/repoHelpers.js";
4
4
 
5
5
  const ajv = new Ajv({ allErrors: true });
@@ -68,8 +68,11 @@ SQUASH:
68
68
  - Útil para manter histórico limpo`;
69
69
 
70
70
  async function handle(args) {
71
+ const emptyHelp = handleEmptyCall(args, inputSchema, "git-merge", { projectPath: "/path/to/project", action: "status" });
72
+ if (emptyHelp) return emptyHelp;
73
+
71
74
  const validate = ajv.compile(inputSchema);
72
- if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
75
+ if (!validate(args)) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
73
76
  const { projectPath, action } = args;
74
77
 
75
78
  try {
@@ -98,11 +101,13 @@ SQUASH:
98
101
  // Pre-merge check: working tree status
99
102
  const status = await git.status(projectPath);
100
103
  if (!status.isClean) {
101
- // We could warn or fail. Standard git refuses merge if changes would be overwritten.
102
- // Let's just warn in logs or return error if critical?
103
- // Git will fail anyway if conflicts with local changes.
104
- // Let's inform the user.
105
- console.warn("[GitMerge] Working tree not clean. Merge might fail.");
104
+ return asToolError("DIRTY_WORKING_TREE",
105
+ "Working tree com alterações não commitadas. Faça commit ou stash antes de mergear.", {
106
+ modified: status.modified,
107
+ staged: status.staged,
108
+ notAdded: status.not_added,
109
+ suggestion: "Use git-workflow action='update' para commitar as mudanças, ou git-stash para guardá-las temporariamente"
110
+ });
106
111
  }
107
112
 
108
113
  const currentBranch = await git.getCurrentBranch(projectPath);
@@ -1,6 +1,6 @@
1
1
  import Ajv from "ajv";
2
2
  import axios from "axios";
3
- import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
3
+ import { asToolError, asToolResult, errorToResponse, handleEmptyCall } from "../utils/errors.js";
4
4
  import { getRepoNameFromPath, validateProjectPath, withRetry } from "../utils/repoHelpers.js";
5
5
  import { runBoth } from "../utils/providerExec.js";
6
6
 
@@ -76,8 +76,11 @@ AÇÕES:
76
76
  NOTA: O PR é criado em AMBOS os providers simultaneamente.`;
77
77
 
78
78
  async function handle(args) {
79
+ const emptyHelp = handleEmptyCall(args, inputSchema, "git-pulls", { projectPath: "/path/to/project", action: "list" });
80
+ if (emptyHelp) return emptyHelp;
81
+
79
82
  const validate = ajv.compile(inputSchema);
80
- if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
83
+ if (!validate(args)) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
81
84
  validateProjectPath(args.projectPath);
82
85
  const repo = getRepoNameFromPath(args.projectPath);
83
86
  try {
@@ -3,7 +3,7 @@ import axios from "axios";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import archiver from "archiver";
6
- import { asToolError, asToolResult, errorToResponse, mapExternalError } from "../utils/errors.js";
6
+ import { asToolError, asToolResult, errorToResponse, mapExternalError, handleEmptyCall } from "../utils/errors.js";
7
7
  import { getRepoNameFromPath, validateProjectPath, withRetry } from "../utils/repoHelpers.js";
8
8
  import { runBoth } from "../utils/providerExec.js";
9
9
  import { sendLog, requestConfirmation } from "../utils/mcpNotify.js";
@@ -167,7 +167,11 @@ ARQUIVOS VIA API:
167
167
  },
168
168
  content: {
169
169
  type: "string",
170
- description: "Conteúdo do arquivo para contents-create"
170
+ description: "Conteúdo do arquivo para contents-create (texto simples, será convertido para base64 automaticamente)"
171
+ },
172
+ message: {
173
+ type: "string",
174
+ description: "Mensagem do commit para contents-create. Default: 'Add {path}'"
171
175
  },
172
176
  branch: {
173
177
  type: "string",
@@ -214,8 +218,11 @@ QUANDO USAR:
214
218
  - Para configurar tópicos: use action='topics-set'`;
215
219
 
216
220
  async function handle(args) {
221
+ const emptyHelp = handleEmptyCall(args, inputSchema, "git-remote", { projectPath: "/path/to/project", action: "list" });
222
+ if (emptyHelp) return emptyHelp;
223
+
217
224
  const validate = ajv.compile(inputSchema);
218
- if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
225
+ if (!validate(args)) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
219
226
  const { projectPath, action } = args;
220
227
  try {
221
228
  validateProjectPath(projectPath);
@@ -1,5 +1,5 @@
1
1
  import Ajv from "ajv";
2
- import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
2
+ import { asToolError, asToolResult, errorToResponse, handleEmptyCall } from "../utils/errors.js";
3
3
  import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
4
4
  import { sendLog, requestConfirmation } from "../utils/mcpNotify.js";
5
5
 
@@ -57,8 +57,11 @@ REFERÊNCIAS:
57
57
  - abc1234: SHA específico do commit`;
58
58
 
59
59
  async function handle(args) {
60
+ const emptyHelp = handleEmptyCall(args, inputSchema, "git-reset", { projectPath: "/path/to/project", action: "soft", ref: "HEAD~1" });
61
+ if (emptyHelp) return emptyHelp;
62
+
60
63
  const validate = ajv.compile(inputSchema);
61
- if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
64
+ if (!validate(args)) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
62
65
  const { projectPath, action, ref } = args;
63
66
  try {
64
67
  validateProjectPath(projectPath);
@@ -1,5 +1,5 @@
1
1
  import Ajv from "ajv";
2
- import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
2
+ import { asToolError, asToolResult, errorToResponse, handleEmptyCall } from "../utils/errors.js";
3
3
  import { validateProjectPath } from "../utils/repoHelpers.js";
4
4
  import { withRetry } from "../utils/retry.js";
5
5
 
@@ -67,8 +67,11 @@ AÇÕES:
67
67
  - clear: Remover todos os stashes`;
68
68
 
69
69
  async function handle(args) {
70
+ const emptyHelp = handleEmptyCall(args, inputSchema, "git-stash", { projectPath: "/path/to/project", action: "list" });
71
+ if (emptyHelp) return emptyHelp;
72
+
70
73
  const validate = ajv.compile(inputSchema);
71
- if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
74
+ if (!validate(args)) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
72
75
  const { projectPath, action } = args;
73
76
  try {
74
77
  validateProjectPath(projectPath);
@@ -1,5 +1,5 @@
1
1
  import Ajv from "ajv";
2
- import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
2
+ import { asToolError, asToolResult, errorToResponse, handleEmptyCall } from "../utils/errors.js";
3
3
  import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
4
4
 
5
5
  const ajv = new Ajv({ allErrors: true });
@@ -54,8 +54,11 @@ FLUXO RECOMENDADO:
54
54
  NOTA: Se pull falhar com conflito, resolva manualmente e faça commit.`;
55
55
 
56
56
  async function handle(args) {
57
+ const emptyHelp = handleEmptyCall(args, inputSchema, "git-sync", { projectPath: "/path/to/project", action: "fetch" });
58
+ if (emptyHelp) return emptyHelp;
59
+
57
60
  const validate = ajv.compile(inputSchema);
58
- if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
61
+ if (!validate(args)) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
59
62
  const { projectPath, action } = args;
60
63
  try {
61
64
  validateProjectPath(projectPath);
@@ -1,5 +1,5 @@
1
1
  import Ajv from "ajv";
2
- import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
2
+ import { asToolError, asToolResult, errorToResponse, handleEmptyCall } from "../utils/errors.js";
3
3
  import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
4
4
 
5
5
  const ajv = new Ajv({ allErrors: true });
@@ -61,8 +61,11 @@ FLUXO TÍPICO:
61
61
  2. git-tags push tag='v1.0.0'`;
62
62
 
63
63
  async function handle(args) {
64
+ const emptyHelp = handleEmptyCall(args, inputSchema, "git-tags", { projectPath: "/path/to/project", action: "list" });
65
+ if (emptyHelp) return emptyHelp;
66
+
64
67
  const validate = ajv.compile(inputSchema);
65
- if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
68
+ if (!validate(args)) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
66
69
  const { projectPath, action } = args;
67
70
  try {
68
71
  validateProjectPath(projectPath);
@@ -1,8 +1,9 @@
1
1
  import Ajv from "ajv";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
- import { asToolError, asToolResult, errorToResponse, createError } from "../utils/errors.js";
4
+ import { asToolError, asToolResult, errorToResponse, createError, handleEmptyCall } from "../utils/errors.js";
5
5
  import { getRepoNameFromPath, detectProjectType, GITIGNORE_TEMPLATES, validateProjectPath, withRetry } from "../utils/repoHelpers.js";
6
+ import { resolveWorktreeContext, isProtectedPath } from "../utils/worktreeResolver.js";
6
7
  import { sendLog } from "../utils/mcpNotify.js";
7
8
 
8
9
  const ajv = new Ajv({ allErrors: true });
@@ -133,13 +134,41 @@ EXEMPLOS DE USO:
133
134
  • Ver mudanças: { "projectPath": "/path/to/project", "action": "status" }`;
134
135
 
135
136
  async function handle(args) {
137
+ // Call vazia = modelo tentando descobrir o schema. Retorna help em vez de erro.
138
+ const emptyHelp = handleEmptyCall(args, inputSchema, "git-workflow", { projectPath: "/path/to/project", action: "update", message: "feat: descrição" });
139
+ if (emptyHelp) return emptyHelp;
140
+
136
141
  const validate = ajv.compile(inputSchema);
137
- if (!validate(args || {})) {
142
+ if (!validate(args)) {
138
143
  return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
139
144
  }
140
145
  const { projectPath, action } = args;
146
+ let effectivePath = projectPath;
147
+ let worktreeCtx = null;
148
+
141
149
  try {
142
150
  validateProjectPath(projectPath);
151
+
152
+ if (action === "init") {
153
+ if (isProtectedPath(projectPath)) {
154
+ return asToolError("PROTECTED_PATH", `Não é permitido criar repositório git em ${projectPath}`);
155
+ }
156
+ } else {
157
+ worktreeCtx = await resolveWorktreeContext(projectPath, git);
158
+ effectivePath = worktreeCtx.worktreePath;
159
+
160
+ // Ajustar paths relativos se estivermos rodando da raiz
161
+ if (effectivePath !== projectPath && Array.isArray(args.files)) {
162
+ const rel = path.relative(effectivePath, projectPath);
163
+ args.files = args.files.map(f => {
164
+ if (path.isAbsolute(f)) return f;
165
+ // Se for ".", vira o próprio diretório relativo
166
+ if (f === ".") return rel.replace(/\\/g, '/');
167
+ return path.join(rel, f).replace(/\\/g, '/');
168
+ });
169
+ }
170
+ }
171
+
143
172
  if (action === "init") {
144
173
  const shouldCreateGitignore = args.createGitignore !== false;
145
174
 
@@ -148,37 +177,37 @@ EXEMPLOS DE USO:
148
177
  success: true,
149
178
  dryRun: true,
150
179
  message: "DRY RUN: Repositório seria inicializado localmente e nos providers",
151
- repoName: getRepoNameFromPath(projectPath),
180
+ repoName: getRepoNameFromPath(effectivePath),
152
181
  gitignoreCreated: shouldCreateGitignore
153
182
  });
154
183
  }
155
184
 
156
- const isRepo = await git.isRepo(projectPath);
185
+ const isRepo = await git.isRepo(effectivePath);
157
186
  if (!isRepo) {
158
- await git.init(projectPath);
187
+ await git.init(effectivePath);
159
188
  }
160
189
 
161
190
  // Criar .gitignore baseado no tipo de projeto (apenas se não existir ou se for novo repo)
162
191
  let gitignoreCreated = false;
163
192
 
164
193
  if (shouldCreateGitignore) {
165
- const hasGitignore = fs.existsSync(path.join(projectPath, ".gitignore"));
194
+ const hasGitignore = fs.existsSync(path.join(effectivePath, ".gitignore"));
166
195
  if (!hasGitignore) {
167
- const projectType = detectProjectType(projectPath);
196
+ const projectType = detectProjectType(effectivePath);
168
197
  const patterns = GITIGNORE_TEMPLATES[projectType] || GITIGNORE_TEMPLATES.general;
169
- await git.createGitignore(projectPath, patterns);
198
+ await git.createGitignore(effectivePath, patterns);
170
199
  gitignoreCreated = true;
171
200
  }
172
201
  }
173
202
 
174
- const repo = getRepoNameFromPath(projectPath);
203
+ const repo = getRepoNameFromPath(effectivePath);
175
204
  const isPublic = args.isPublic === true; // Default: privado
176
205
  const organization = args.organization || undefined;
177
206
  const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic, organization });
178
207
 
179
208
  // Configurar remotes com org se fornecida
180
209
  const urls = await pm.getRemoteUrls(repo, organization);
181
- await git.ensureRemotes(projectPath, { githubUrl: urls.github || "", giteaUrl: urls.gitea || "" });
210
+ await git.ensureRemotes(effectivePath, { githubUrl: urls.github || "", giteaUrl: urls.gitea || "" });
182
211
 
183
212
  return asToolResult({
184
213
  success: true,
@@ -190,7 +219,7 @@ EXEMPLOS DE USO:
190
219
  });
191
220
  }
192
221
  if (action === "status") {
193
- const st = await git.status(projectPath);
222
+ const st = await git.status(effectivePath);
194
223
 
195
224
  if (args.dryRun) {
196
225
  return asToolResult({
@@ -224,7 +253,15 @@ EXEMPLOS DE USO:
224
253
  _aiContext.suggestedAction = "Working tree limpa. Modifique arquivos ou use action='push' se há commits pendentes";
225
254
  }
226
255
 
227
- return asToolResult({ ...st, _aiContext }, { tool: 'workflow', action: 'status' });
256
+ const result = { ...st, _aiContext };
257
+ if (worktreeCtx?.isWorktree) {
258
+ result._worktreeContext = {
259
+ detected: true,
260
+ repoRoot: worktreeCtx.repoRoot,
261
+ branch: worktreeCtx.branch
262
+ };
263
+ }
264
+ return asToolResult(result, { tool: 'workflow', action: 'status' });
228
265
  }
229
266
  if (action === "add") {
230
267
  const files = Array.isArray(args.files) && args.files.length ? args.files : ["."];
@@ -238,12 +275,12 @@ EXEMPLOS DE USO:
238
275
  });
239
276
  }
240
277
 
241
- await git.add(projectPath, files);
278
+ await git.add(effectivePath, files);
242
279
  return asToolResult({ success: true, files }, { tool: 'workflow', action: 'add' });
243
280
  }
244
281
  if (action === "remove") {
245
282
  const files = Array.isArray(args.files) ? args.files : [];
246
- await git.remove(projectPath, files);
283
+ await git.remove(effectivePath, files);
247
284
  return asToolResult({ success: true, files });
248
285
  }
249
286
  if (action === "commit") {
@@ -259,12 +296,20 @@ EXEMPLOS DE USO:
259
296
  });
260
297
  }
261
298
 
262
- const sha = await git.commit(projectPath, args.message);
299
+ const st = await git.status(effectivePath);
300
+ if (!st.staged || st.staged.length === 0) {
301
+ return asToolError("NOTHING_TO_COMMIT", "Nenhum arquivo staged para commitar", {
302
+ suggestion: "Use action='add' com files=['.'] para adicionar arquivos ao staging antes de commitar",
303
+ status: { modified: st.modified, notAdded: st.not_added }
304
+ });
305
+ }
306
+
307
+ const sha = await git.commit(effectivePath, args.message);
263
308
  return asToolResult({ success: true, sha, message: args.message }, { tool: 'workflow', action: 'commit' });
264
309
  }
265
310
  if (action === "clean") {
266
311
  if (args.dryRun) {
267
- const result = await git.cleanUntracked(projectPath);
312
+ const result = await git.cleanUntracked(effectivePath);
268
313
  return asToolResult({
269
314
  success: true,
270
315
  dryRun: true,
@@ -273,7 +318,7 @@ EXEMPLOS DE USO:
273
318
  });
274
319
  }
275
320
 
276
- const result = await git.cleanUntracked(projectPath);
321
+ const result = await git.cleanUntracked(effectivePath);
277
322
  return asToolResult({
278
323
  success: true,
279
324
  ...result,
@@ -283,7 +328,7 @@ EXEMPLOS DE USO:
283
328
  });
284
329
  }
285
330
  if (action === "ensure-remotes") {
286
- const repo = getRepoNameFromPath(projectPath);
331
+ const repo = getRepoNameFromPath(effectivePath);
287
332
  const isPublic = args.isPublic === true; // Default: privado
288
333
  const organization = args.organization || undefined;
289
334
 
@@ -310,12 +355,12 @@ EXEMPLOS DE USO:
310
355
  const giteaBase = pm.giteaUrl?.replace(/\/$/, "");
311
356
  const giteaUrl = ensured.gitea?.ok && ensured.gitea.repo && giteaBase
312
357
  ? `${giteaBase}/${ensured.gitea.repo}.git` : "";
313
- await git.ensureRemotes(projectPath, { githubUrl, giteaUrl, organization });
314
- const remotes = await git.listRemotes(projectPath);
358
+ await git.ensureRemotes(effectivePath, { githubUrl, giteaUrl, organization });
359
+ const remotes = await git.listRemotes(effectivePath);
315
360
  return asToolResult({ success: true, ensured, remotes, isPrivate: !isPublic, organization: organization || undefined }, { tool: 'workflow', action: 'ensure-remotes' });
316
361
  }
317
362
  if (action === "push") {
318
- const branch = await git.getCurrentBranch(projectPath);
363
+ const branch = await git.getCurrentBranch(effectivePath);
319
364
  const force = !!args.force;
320
365
 
321
366
  if (args.dryRun) {
@@ -335,7 +380,7 @@ EXEMPLOS DE USO:
335
380
 
336
381
  // Retry logic for push (often fails due to network or concurrent updates)
337
382
  const result = await withRetry(
338
- () => git.pushParallel(projectPath, branch, force, organization),
383
+ () => git.pushParallel(effectivePath, branch, force, organization),
339
384
  3,
340
385
  "push"
341
386
  );
@@ -350,15 +395,15 @@ EXEMPLOS DE USO:
350
395
  }
351
396
 
352
397
  const channel = args.channel || "production";
353
- const wtPath = path.join(projectPath, branch);
398
+ const wtPath = path.join(projectPath, branch); // Cria relativo ao path original solicitado
354
399
 
355
400
  if (fs.existsSync(wtPath)) {
356
401
  return asToolError("WORKTREE_PATH_EXISTS", `Diretório '${wtPath}' já existe.`, { suggestion: "Use git-worktree add se quiser configurar path customizado" });
357
402
  }
358
403
 
359
404
  try {
360
- await git.addWorktree(projectPath, branch, wtPath);
361
- await git.setWorktreeConfig(projectPath, branch, { path: wtPath, channel });
405
+ await git.addWorktree(effectivePath, branch, wtPath);
406
+ await git.setWorktreeConfig(effectivePath, branch, { path: wtPath, channel });
362
407
 
363
408
  return asToolResult({
364
409
  success: true,
@@ -390,13 +435,13 @@ EXEMPLOS DE USO:
390
435
  if (args.dryRun) {
391
436
  gitignored = gitignorePatterns;
392
437
  } else {
393
- await git.addToGitignore(projectPath, gitignorePatterns);
438
+ await git.addToGitignore(effectivePath, gitignorePatterns);
394
439
  gitignored = gitignorePatterns;
395
440
  }
396
441
  }
397
442
 
398
443
  // 1. Status para ver o que mudou
399
- const status = await git.status(projectPath);
444
+ const status = await git.status(effectivePath);
400
445
 
401
446
  const hasChanges = !status.isClean ||
402
447
  (status.not_added?.length > 0) ||
@@ -438,15 +483,15 @@ EXEMPLOS DE USO:
438
483
  }
439
484
 
440
485
  // 2. Add
441
- await git.add(projectPath, files);
486
+ await git.add(effectivePath, files);
442
487
 
443
488
  // 3. Commit
444
- const sha = await git.commit(projectPath, args.message);
489
+ const sha = await git.commit(effectivePath, args.message);
445
490
 
446
491
  // 3.5. Garantir remotes com organization (se fornecida)
447
492
  const organization = args.organization || undefined;
448
493
  if (organization) {
449
- const repo = getRepoNameFromPath(projectPath);
494
+ const repo = getRepoNameFromPath(effectivePath);
450
495
  const isPublic = args.isPublic === true;
451
496
  const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic, organization });
452
497
  // Build URLs from actual ensureRepos results (handles GitHub fallback to personal account)
@@ -455,11 +500,11 @@ EXEMPLOS DE USO:
455
500
  const giteaBase = pm.giteaUrl?.replace(/\/$/, "");
456
501
  const giteaUrl = ensured.gitea?.ok && ensured.gitea.repo && giteaBase
457
502
  ? `${giteaBase}/${ensured.gitea.repo}.git` : "";
458
- await git.ensureRemotes(projectPath, { githubUrl, giteaUrl, organization });
503
+ await git.ensureRemotes(effectivePath, { githubUrl, giteaUrl, organization });
459
504
  }
460
505
 
461
506
  // 4. Push Strategy
462
- const branch = await git.getCurrentBranch(projectPath);
507
+ const branch = await git.getCurrentBranch(effectivePath);
463
508
  let pushResult = {};
464
509
  let synced = [];
465
510
  let errors = [];
@@ -467,7 +512,7 @@ EXEMPLOS DE USO:
467
512
  // Determine Channel
468
513
  let channel = args.channel;
469
514
  if (!channel) {
470
- const storedChannel = await git.getConfig(projectPath, `worktree-branch.${branch}.channel`);
515
+ const storedChannel = await git.getConfig(effectivePath, `worktree-branch.${branch}.channel`);
471
516
  if (storedChannel) channel = storedChannel;
472
517
  }
473
518
 
@@ -477,13 +522,13 @@ EXEMPLOS DE USO:
477
522
  // SYNC BRANCHES
478
523
  if (syncBranches) {
479
524
  // Só permitido no principal (onde .git é diretório)
480
- if (fs.existsSync(path.join(projectPath, ".git")) && fs.statSync(path.join(projectPath, ".git")).isFile()) {
525
+ if (fs.existsSync(path.join(effectivePath, ".git")) && fs.statSync(path.join(effectivePath, ".git")).isFile()) {
481
526
  return asToolError("INVALID_OPERATION", "syncBranches=true só pode ser executado a partir do repositório principal, não de um worktree.");
482
527
  }
483
528
 
484
529
  // 1. Push do principal (current)
485
530
  const remoteBranch = resolveRemoteBranch(branch, channel);
486
- const remotes = await git.listRemotes(projectPath);
531
+ const remotes = await git.listRemotes(effectivePath);
487
532
 
488
533
  await sendLog(server, "info", "update: push iniciado (sync)", { branch, remoteBranch, force });
489
534
 
@@ -491,7 +536,7 @@ EXEMPLOS DE USO:
491
536
  const mainFailed = [];
492
537
  for (const r of remotes) {
493
538
  try {
494
- await git.pushRefspec(projectPath, r.remote, branch, remoteBranch, force);
539
+ await git.pushRefspec(effectivePath, r.remote, branch, remoteBranch, force);
495
540
  mainPushed.push(r.remote);
496
541
  } catch(e) {
497
542
  mainFailed.push({ remote: r.remote, error: e.message });
@@ -501,7 +546,7 @@ EXEMPLOS DE USO:
501
546
  // 2. Propagar para worktrees
502
547
  // Req 8.3: synced.length + errors.length === N (número de worktrees registrados)
503
548
  // O principal NÃO entra no contador — só os worktrees registrados
504
- const configs = await git.getWorktreeConfigs(projectPath);
549
+ const configs = await git.getWorktreeConfigs(effectivePath);
505
550
 
506
551
  for (const wt of configs) {
507
552
  // Pula se for a própria branch principal (caso esteja registrada)
@@ -538,13 +583,13 @@ EXEMPLOS DE USO:
538
583
  // Comportamento padrão (production)
539
584
  await sendLog(server, "info", "update: push iniciado", { branch, force });
540
585
  pushResult = await withRetry(
541
- () => git.pushParallel(projectPath, branch, force, organization),
586
+ () => git.pushParallel(effectivePath, branch, force, organization),
542
587
  3,
543
588
  "push"
544
589
  );
545
590
  } else {
546
591
  // Comportamento customizado (beta/alpha) -> pushRefspec
547
- const remotes = await git.listRemotes(projectPath);
592
+ const remotes = await git.listRemotes(effectivePath);
548
593
 
549
594
  const pushed = [];
550
595
  const failed = [];
@@ -553,7 +598,7 @@ EXEMPLOS DE USO:
553
598
 
554
599
  for (const r of remotes) {
555
600
  try {
556
- await git.pushRefspec(projectPath, r.remote, branch, remoteBranch, force);
601
+ await git.pushRefspec(effectivePath, r.remote, branch, remoteBranch, force);
557
602
  pushed.push(r.remote);
558
603
  } catch (e) {
559
604
  failed.push({ remote: r.remote, error: e.message });
@@ -568,7 +613,7 @@ EXEMPLOS DE USO:
568
613
  }
569
614
  }
570
615
 
571
- return asToolResult({
616
+ const result = {
572
617
  success: true,
573
618
  action: "update",
574
619
  steps: ["status", "add", "commit", "push"],
@@ -583,7 +628,17 @@ EXEMPLOS DE USO:
583
628
  completed: true,
584
629
  message: "Ciclo completo: arquivos adicionados, commit criado e push realizado" + (organization ? ` [org: ${organization}]` : "")
585
630
  }
586
- }, { tool: 'workflow', action: 'update' });
631
+ };
632
+
633
+ if (worktreeCtx?.isWorktree) {
634
+ result._worktreeContext = {
635
+ detected: true,
636
+ repoRoot: worktreeCtx.repoRoot,
637
+ branch: worktreeCtx.branch
638
+ };
639
+ }
640
+
641
+ return asToolResult(result, { tool: 'workflow', action: 'update' });
587
642
  }
588
643
 
589
644
  return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {