@andre.buzeli/git-mcp 16.1.2 → 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.
@@ -3,6 +3,7 @@ import fs from "fs";
3
3
  import path from "path";
4
4
  import { asToolError, asToolResult, errorToResponse, createError } 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 });
@@ -40,7 +41,8 @@ export function createGitWorkflowTool(pm, git, server) {
40
41
  files: {
41
42
  type: "array",
42
43
  items: { type: "string" },
43
- description: "Lista de arquivos para add/remove. Use ['.'] para todos os arquivos. Ex: ['src/index.js', 'package.json']"
44
+ default: ["."],
45
+ description: "Lista de arquivos para add/remove. Default: ['.'] (todos). Ex: ['src/index.js', 'package.json']"
44
46
  },
45
47
  message: {
46
48
  type: "string",
@@ -61,22 +63,27 @@ export function createGitWorkflowTool(pm, git, server) {
61
63
  },
62
64
  force: {
63
65
  type: "boolean",
66
+ default: false,
64
67
  description: "Force push (use apenas se push normal falhar com erro de histórico divergente). Default: false"
65
68
  },
66
69
  createGitignore: {
67
70
  type: "boolean",
71
+ default: true,
68
72
  description: "Se true, cria .gitignore padrão baseado no tipo de projeto (action='init'). Default: true"
69
73
  },
70
74
  isPublic: {
71
75
  type: "boolean",
76
+ default: false,
72
77
  description: "Se true, repositório será PÚBLICO. Default: false (privado). Aplica-se a action='init' e 'ensure-remotes'"
73
78
  },
74
79
  dryRun: {
75
80
  type: "boolean",
81
+ default: false,
76
82
  description: "Se true, simula a operação sem executar (útil para testes). Default: false"
77
83
  },
78
84
  skipIfClean: {
79
85
  type: "boolean",
86
+ default: false,
80
87
  description: "Para action='update': se true, pula silenciosamente se não houver mudanças. Default: false"
81
88
  },
82
89
  gitignore: {
@@ -132,8 +139,32 @@ EXEMPLOS DE USO:
132
139
  return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
133
140
  }
134
141
  const { projectPath, action } = args;
142
+ let effectivePath = projectPath;
143
+ let worktreeCtx = null;
144
+
135
145
  try {
136
146
  validateProjectPath(projectPath);
147
+
148
+ if (action === "init") {
149
+ if (isProtectedPath(projectPath)) {
150
+ return asToolError("PROTECTED_PATH", `Não é permitido criar repositório git em ${projectPath}`);
151
+ }
152
+ } else {
153
+ worktreeCtx = await resolveWorktreeContext(projectPath, git);
154
+ effectivePath = worktreeCtx.worktreePath;
155
+
156
+ // Ajustar paths relativos se estivermos rodando da raiz
157
+ if (effectivePath !== projectPath && Array.isArray(args.files)) {
158
+ const rel = path.relative(effectivePath, projectPath);
159
+ args.files = args.files.map(f => {
160
+ if (path.isAbsolute(f)) return f;
161
+ // Se for ".", vira o próprio diretório relativo
162
+ if (f === ".") return rel.replace(/\\/g, '/');
163
+ return path.join(rel, f).replace(/\\/g, '/');
164
+ });
165
+ }
166
+ }
167
+
137
168
  if (action === "init") {
138
169
  const shouldCreateGitignore = args.createGitignore !== false;
139
170
 
@@ -142,37 +173,37 @@ EXEMPLOS DE USO:
142
173
  success: true,
143
174
  dryRun: true,
144
175
  message: "DRY RUN: Repositório seria inicializado localmente e nos providers",
145
- repoName: getRepoNameFromPath(projectPath),
176
+ repoName: getRepoNameFromPath(effectivePath),
146
177
  gitignoreCreated: shouldCreateGitignore
147
178
  });
148
179
  }
149
180
 
150
- const isRepo = await git.isRepo(projectPath);
181
+ const isRepo = await git.isRepo(effectivePath);
151
182
  if (!isRepo) {
152
- await git.init(projectPath);
183
+ await git.init(effectivePath);
153
184
  }
154
185
 
155
186
  // Criar .gitignore baseado no tipo de projeto (apenas se não existir ou se for novo repo)
156
187
  let gitignoreCreated = false;
157
188
 
158
189
  if (shouldCreateGitignore) {
159
- const hasGitignore = fs.existsSync(path.join(projectPath, ".gitignore"));
190
+ const hasGitignore = fs.existsSync(path.join(effectivePath, ".gitignore"));
160
191
  if (!hasGitignore) {
161
- const projectType = detectProjectType(projectPath);
192
+ const projectType = detectProjectType(effectivePath);
162
193
  const patterns = GITIGNORE_TEMPLATES[projectType] || GITIGNORE_TEMPLATES.general;
163
- await git.createGitignore(projectPath, patterns);
194
+ await git.createGitignore(effectivePath, patterns);
164
195
  gitignoreCreated = true;
165
196
  }
166
197
  }
167
198
 
168
- const repo = getRepoNameFromPath(projectPath);
199
+ const repo = getRepoNameFromPath(effectivePath);
169
200
  const isPublic = args.isPublic === true; // Default: privado
170
201
  const organization = args.organization || undefined;
171
202
  const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic, organization });
172
203
 
173
204
  // Configurar remotes com org se fornecida
174
205
  const urls = await pm.getRemoteUrls(repo, organization);
175
- await git.ensureRemotes(projectPath, { githubUrl: urls.github || "", giteaUrl: urls.gitea || "" });
206
+ await git.ensureRemotes(effectivePath, { githubUrl: urls.github || "", giteaUrl: urls.gitea || "" });
176
207
 
177
208
  return asToolResult({
178
209
  success: true,
@@ -184,7 +215,7 @@ EXEMPLOS DE USO:
184
215
  });
185
216
  }
186
217
  if (action === "status") {
187
- const st = await git.status(projectPath);
218
+ const st = await git.status(effectivePath);
188
219
 
189
220
  if (args.dryRun) {
190
221
  return asToolResult({
@@ -218,7 +249,15 @@ EXEMPLOS DE USO:
218
249
  _aiContext.suggestedAction = "Working tree limpa. Modifique arquivos ou use action='push' se há commits pendentes";
219
250
  }
220
251
 
221
- return asToolResult({ ...st, _aiContext }, { tool: 'workflow', action: 'status' });
252
+ const result = { ...st, _aiContext };
253
+ if (worktreeCtx?.isWorktree) {
254
+ result._worktreeContext = {
255
+ detected: true,
256
+ repoRoot: worktreeCtx.repoRoot,
257
+ branch: worktreeCtx.branch
258
+ };
259
+ }
260
+ return asToolResult(result, { tool: 'workflow', action: 'status' });
222
261
  }
223
262
  if (action === "add") {
224
263
  const files = Array.isArray(args.files) && args.files.length ? args.files : ["."];
@@ -232,12 +271,12 @@ EXEMPLOS DE USO:
232
271
  });
233
272
  }
234
273
 
235
- await git.add(projectPath, files);
274
+ await git.add(effectivePath, files);
236
275
  return asToolResult({ success: true, files }, { tool: 'workflow', action: 'add' });
237
276
  }
238
277
  if (action === "remove") {
239
278
  const files = Array.isArray(args.files) ? args.files : [];
240
- await git.remove(projectPath, files);
279
+ await git.remove(effectivePath, files);
241
280
  return asToolResult({ success: true, files });
242
281
  }
243
282
  if (action === "commit") {
@@ -253,12 +292,20 @@ EXEMPLOS DE USO:
253
292
  });
254
293
  }
255
294
 
256
- const sha = await git.commit(projectPath, args.message);
295
+ const st = await git.status(effectivePath);
296
+ if (!st.staged || st.staged.length === 0) {
297
+ return asToolError("NOTHING_TO_COMMIT", "Nenhum arquivo staged para commitar", {
298
+ suggestion: "Use action='add' com files=['.'] para adicionar arquivos ao staging antes de commitar",
299
+ status: { modified: st.modified, notAdded: st.not_added }
300
+ });
301
+ }
302
+
303
+ const sha = await git.commit(effectivePath, args.message);
257
304
  return asToolResult({ success: true, sha, message: args.message }, { tool: 'workflow', action: 'commit' });
258
305
  }
259
306
  if (action === "clean") {
260
307
  if (args.dryRun) {
261
- const result = await git.cleanUntracked(projectPath);
308
+ const result = await git.cleanUntracked(effectivePath);
262
309
  return asToolResult({
263
310
  success: true,
264
311
  dryRun: true,
@@ -267,7 +314,7 @@ EXEMPLOS DE USO:
267
314
  });
268
315
  }
269
316
 
270
- const result = await git.cleanUntracked(projectPath);
317
+ const result = await git.cleanUntracked(effectivePath);
271
318
  return asToolResult({
272
319
  success: true,
273
320
  ...result,
@@ -277,7 +324,7 @@ EXEMPLOS DE USO:
277
324
  });
278
325
  }
279
326
  if (action === "ensure-remotes") {
280
- const repo = getRepoNameFromPath(projectPath);
327
+ const repo = getRepoNameFromPath(effectivePath);
281
328
  const isPublic = args.isPublic === true; // Default: privado
282
329
  const organization = args.organization || undefined;
283
330
 
@@ -304,12 +351,12 @@ EXEMPLOS DE USO:
304
351
  const giteaBase = pm.giteaUrl?.replace(/\/$/, "");
305
352
  const giteaUrl = ensured.gitea?.ok && ensured.gitea.repo && giteaBase
306
353
  ? `${giteaBase}/${ensured.gitea.repo}.git` : "";
307
- await git.ensureRemotes(projectPath, { githubUrl, giteaUrl, organization });
308
- const remotes = await git.listRemotes(projectPath);
354
+ await git.ensureRemotes(effectivePath, { githubUrl, giteaUrl, organization });
355
+ const remotes = await git.listRemotes(effectivePath);
309
356
  return asToolResult({ success: true, ensured, remotes, isPrivate: !isPublic, organization: organization || undefined }, { tool: 'workflow', action: 'ensure-remotes' });
310
357
  }
311
358
  if (action === "push") {
312
- const branch = await git.getCurrentBranch(projectPath);
359
+ const branch = await git.getCurrentBranch(effectivePath);
313
360
  const force = !!args.force;
314
361
 
315
362
  if (args.dryRun) {
@@ -329,7 +376,7 @@ EXEMPLOS DE USO:
329
376
 
330
377
  // Retry logic for push (often fails due to network or concurrent updates)
331
378
  const result = await withRetry(
332
- () => git.pushParallel(projectPath, branch, force, organization),
379
+ () => git.pushParallel(effectivePath, branch, force, organization),
333
380
  3,
334
381
  "push"
335
382
  );
@@ -344,15 +391,15 @@ EXEMPLOS DE USO:
344
391
  }
345
392
 
346
393
  const channel = args.channel || "production";
347
- const wtPath = path.join(projectPath, branch);
394
+ const wtPath = path.join(projectPath, branch); // Cria relativo ao path original solicitado
348
395
 
349
396
  if (fs.existsSync(wtPath)) {
350
397
  return asToolError("WORKTREE_PATH_EXISTS", `Diretório '${wtPath}' já existe.`, { suggestion: "Use git-worktree add se quiser configurar path customizado" });
351
398
  }
352
399
 
353
400
  try {
354
- await git.addWorktree(projectPath, branch, wtPath);
355
- await git.setWorktreeConfig(projectPath, branch, { path: wtPath, channel });
401
+ await git.addWorktree(effectivePath, branch, wtPath);
402
+ await git.setWorktreeConfig(effectivePath, branch, { path: wtPath, channel });
356
403
 
357
404
  return asToolResult({
358
405
  success: true,
@@ -384,13 +431,13 @@ EXEMPLOS DE USO:
384
431
  if (args.dryRun) {
385
432
  gitignored = gitignorePatterns;
386
433
  } else {
387
- await git.addToGitignore(projectPath, gitignorePatterns);
434
+ await git.addToGitignore(effectivePath, gitignorePatterns);
388
435
  gitignored = gitignorePatterns;
389
436
  }
390
437
  }
391
438
 
392
439
  // 1. Status para ver o que mudou
393
- const status = await git.status(projectPath);
440
+ const status = await git.status(effectivePath);
394
441
 
395
442
  const hasChanges = !status.isClean ||
396
443
  (status.not_added?.length > 0) ||
@@ -432,15 +479,15 @@ EXEMPLOS DE USO:
432
479
  }
433
480
 
434
481
  // 2. Add
435
- await git.add(projectPath, files);
482
+ await git.add(effectivePath, files);
436
483
 
437
484
  // 3. Commit
438
- const sha = await git.commit(projectPath, args.message);
485
+ const sha = await git.commit(effectivePath, args.message);
439
486
 
440
487
  // 3.5. Garantir remotes com organization (se fornecida)
441
488
  const organization = args.organization || undefined;
442
489
  if (organization) {
443
- const repo = getRepoNameFromPath(projectPath);
490
+ const repo = getRepoNameFromPath(effectivePath);
444
491
  const isPublic = args.isPublic === true;
445
492
  const ensured = await pm.ensureRepos({ repoName: repo, createIfMissing: true, isPublic, organization });
446
493
  // Build URLs from actual ensureRepos results (handles GitHub fallback to personal account)
@@ -449,11 +496,11 @@ EXEMPLOS DE USO:
449
496
  const giteaBase = pm.giteaUrl?.replace(/\/$/, "");
450
497
  const giteaUrl = ensured.gitea?.ok && ensured.gitea.repo && giteaBase
451
498
  ? `${giteaBase}/${ensured.gitea.repo}.git` : "";
452
- await git.ensureRemotes(projectPath, { githubUrl, giteaUrl, organization });
499
+ await git.ensureRemotes(effectivePath, { githubUrl, giteaUrl, organization });
453
500
  }
454
501
 
455
502
  // 4. Push Strategy
456
- const branch = await git.getCurrentBranch(projectPath);
503
+ const branch = await git.getCurrentBranch(effectivePath);
457
504
  let pushResult = {};
458
505
  let synced = [];
459
506
  let errors = [];
@@ -461,7 +508,7 @@ EXEMPLOS DE USO:
461
508
  // Determine Channel
462
509
  let channel = args.channel;
463
510
  if (!channel) {
464
- const storedChannel = await git.getConfig(projectPath, `worktree-branch.${branch}.channel`);
511
+ const storedChannel = await git.getConfig(effectivePath, `worktree-branch.${branch}.channel`);
465
512
  if (storedChannel) channel = storedChannel;
466
513
  }
467
514
 
@@ -471,13 +518,13 @@ EXEMPLOS DE USO:
471
518
  // SYNC BRANCHES
472
519
  if (syncBranches) {
473
520
  // Só permitido no principal (onde .git é diretório)
474
- if (fs.existsSync(path.join(projectPath, ".git")) && fs.statSync(path.join(projectPath, ".git")).isFile()) {
521
+ if (fs.existsSync(path.join(effectivePath, ".git")) && fs.statSync(path.join(effectivePath, ".git")).isFile()) {
475
522
  return asToolError("INVALID_OPERATION", "syncBranches=true só pode ser executado a partir do repositório principal, não de um worktree.");
476
523
  }
477
524
 
478
525
  // 1. Push do principal (current)
479
526
  const remoteBranch = resolveRemoteBranch(branch, channel);
480
- const remotes = await git.listRemotes(projectPath);
527
+ const remotes = await git.listRemotes(effectivePath);
481
528
 
482
529
  await sendLog(server, "info", "update: push iniciado (sync)", { branch, remoteBranch, force });
483
530
 
@@ -485,7 +532,7 @@ EXEMPLOS DE USO:
485
532
  const mainFailed = [];
486
533
  for (const r of remotes) {
487
534
  try {
488
- await git.pushRefspec(projectPath, r.remote, branch, remoteBranch, force);
535
+ await git.pushRefspec(effectivePath, r.remote, branch, remoteBranch, force);
489
536
  mainPushed.push(r.remote);
490
537
  } catch(e) {
491
538
  mainFailed.push({ remote: r.remote, error: e.message });
@@ -495,7 +542,7 @@ EXEMPLOS DE USO:
495
542
  // 2. Propagar para worktrees
496
543
  // Req 8.3: synced.length + errors.length === N (número de worktrees registrados)
497
544
  // O principal NÃO entra no contador — só os worktrees registrados
498
- const configs = await git.getWorktreeConfigs(projectPath);
545
+ const configs = await git.getWorktreeConfigs(effectivePath);
499
546
 
500
547
  for (const wt of configs) {
501
548
  // Pula se for a própria branch principal (caso esteja registrada)
@@ -532,13 +579,13 @@ EXEMPLOS DE USO:
532
579
  // Comportamento padrão (production)
533
580
  await sendLog(server, "info", "update: push iniciado", { branch, force });
534
581
  pushResult = await withRetry(
535
- () => git.pushParallel(projectPath, branch, force, organization),
582
+ () => git.pushParallel(effectivePath, branch, force, organization),
536
583
  3,
537
584
  "push"
538
585
  );
539
586
  } else {
540
587
  // Comportamento customizado (beta/alpha) -> pushRefspec
541
- const remotes = await git.listRemotes(projectPath);
588
+ const remotes = await git.listRemotes(effectivePath);
542
589
 
543
590
  const pushed = [];
544
591
  const failed = [];
@@ -547,7 +594,7 @@ EXEMPLOS DE USO:
547
594
 
548
595
  for (const r of remotes) {
549
596
  try {
550
- await git.pushRefspec(projectPath, r.remote, branch, remoteBranch, force);
597
+ await git.pushRefspec(effectivePath, r.remote, branch, remoteBranch, force);
551
598
  pushed.push(r.remote);
552
599
  } catch (e) {
553
600
  failed.push({ remote: r.remote, error: e.message });
@@ -562,7 +609,7 @@ EXEMPLOS DE USO:
562
609
  }
563
610
  }
564
611
 
565
- return asToolResult({
612
+ const result = {
566
613
  success: true,
567
614
  action: "update",
568
615
  steps: ["status", "add", "commit", "push"],
@@ -577,7 +624,17 @@ EXEMPLOS DE USO:
577
624
  completed: true,
578
625
  message: "Ciclo completo: arquivos adicionados, commit criado e push realizado" + (organization ? ` [org: ${organization}]` : "")
579
626
  }
580
- }, { tool: 'workflow', action: 'update' });
627
+ };
628
+
629
+ if (worktreeCtx?.isWorktree) {
630
+ result._worktreeContext = {
631
+ detected: true,
632
+ repoRoot: worktreeCtx.repoRoot,
633
+ branch: worktreeCtx.branch
634
+ };
635
+ }
636
+
637
+ return asToolResult(result, { tool: 'workflow', action: 'update' });
581
638
  }
582
639
 
583
640
  return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, {
@@ -3,6 +3,7 @@ import fs from "fs";
3
3
  import path from "path";
4
4
  import { asToolError, asToolResult, errorToResponse } from "../utils/errors.js";
5
5
  import { validateProjectPath, withRetry } from "../utils/repoHelpers.js";
6
+ import { resolveWorktreeContext, isProtectedPath } from "../utils/worktreeResolver.js";
6
7
 
7
8
  const ajv = new Ajv({ allErrors: true });
8
9
 
@@ -75,62 +76,59 @@ AÇÕES:
75
76
  const validate = ajv.compile(inputSchema);
76
77
  if (!validate(args || {})) return asToolError("VALIDATION_ERROR", "Parâmetros inválidos", validate.errors);
77
78
  const { projectPath, action } = args;
79
+ let effectivePath = projectPath;
80
+ let worktreeCtx = null;
78
81
 
79
82
  try {
80
83
  validateProjectPath(projectPath);
81
84
 
85
+ worktreeCtx = await resolveWorktreeContext(projectPath, git);
86
+ effectivePath = worktreeCtx.worktreePath;
87
+
88
+ const _worktreeContext = worktreeCtx.isWorktree ? {
89
+ detected: true,
90
+ repoRoot: worktreeCtx.repoRoot,
91
+ branch: worktreeCtx.branch
92
+ } : undefined;
93
+
82
94
  if (action === "list") {
83
- const worktrees = await git.listWorktrees(projectPath);
95
+ const worktrees = await git.listWorktrees(effectivePath);
84
96
 
85
97
  // Enrich with channel info if available
86
98
  const enriched = [];
87
99
  for (const wt of worktrees) {
88
- const channel = await git.getConfig(projectPath, `worktree-branch.${wt.branch}.channel`);
100
+ const channel = await git.getConfig(effectivePath, `worktree-branch.${wt.branch}.channel`);
89
101
  enriched.push({ ...wt, channel: channel || "production" }); // Default to production
90
102
  }
91
103
 
92
- return asToolResult({ worktrees: enriched, count: enriched.length });
104
+ return asToolResult({ worktrees: enriched, count: enriched.length, _worktreeContext });
93
105
  }
94
106
 
95
107
  if (action === "add") {
96
108
  if (!args.branch) return asToolError("MISSING_PARAMETER", "branch é obrigatório para add", { parameter: "branch" });
97
109
 
98
- // Default path: sibling directory with branch name
99
- // Se projectPath é .../repo/main, e branch é 'feature', cria .../repo/feature
100
- // Mas git worktree add path branch.
101
-
102
- // Se path não fornecido, assumir irmão do diretório atual se estivermos em um repo.
103
- // Mas projectPath é a raiz do repo.
104
- // Vamos usar path relativo ou absoluto fornecido, ou default: join(projectPath, "..", branch)
105
-
106
- const targetPath = args.path
110
+ // Cria relativo ao projectPath original se path não for absoluto
111
+ const wtPath = args.path
107
112
  ? path.resolve(projectPath, args.path)
108
- : path.join(projectPath, args.branch); // Cria DENTRO do projeto? Não, worktrees geralmente ficam ao lado ou fora.
109
-
110
- // Melhor prática: criar dentro se for estrutura monorepo-style, ou fora.
111
- // O git-mcp v16 parece usar estrutura plana ou aninhada.
112
- // Vamos usar o comportamento padrão do git: path relativo à raiz.
113
- // Se o usuário passar apenas o nome da branch como path, o git cria ./branch-name.
114
-
115
- // Se path não informado, usa o nome da branch (cria pasta ./branch-name dentro do repo atual)
116
- // Isso pode sujar o repo principal se não estiver no gitignore.
117
- // Vamos sugerir ou usar ../branch-name se estivermos na raiz?
118
- // Simplicidade: user define path ou usa ./branch-name.
119
-
120
- const wtPath = args.path || args.branch;
113
+ : path.join(projectPath, args.branch);
121
114
 
122
- await withRetry(() => git.addWorktree(projectPath, args.branch, wtPath, args.force), 3, "worktree-add");
115
+ if (isProtectedPath(wtPath)) {
116
+ return asToolError("PROTECTED_PATH", `Não é permitido criar worktree em ${wtPath}`);
117
+ }
118
+
119
+ await withRetry(() => git.addWorktree(effectivePath, args.branch, wtPath, args.force), 3, "worktree-add");
123
120
 
124
121
  // Set channel if provided
125
122
  if (args.channel) {
126
- await git.setWorktreeConfig(projectPath, args.branch, { channel: args.channel });
123
+ await git.setWorktreeConfig(effectivePath, args.branch, { channel: args.channel });
127
124
  }
128
125
 
129
126
  return asToolResult({
130
127
  success: true,
131
128
  branch: args.branch,
132
- path: path.resolve(projectPath, wtPath),
133
- channel: args.channel || "production"
129
+ path: wtPath,
130
+ channel: args.channel || "production",
131
+ _worktreeContext
134
132
  });
135
133
  }
136
134
 
@@ -141,27 +139,54 @@ AÇÕES:
141
139
  // Precisamos encontrar o path se só branch for fornecida
142
140
  let targetPath = args.path;
143
141
  if (!targetPath && args.branch) {
144
- const wts = await git.listWorktrees(projectPath);
142
+ const wts = await git.listWorktrees(effectivePath);
145
143
  const found = wts.find(w => w.branch === args.branch);
146
144
  if (!found) return asToolError("WORKTREE_NOT_FOUND", `Worktree para branch '${args.branch}' não encontrado`);
147
145
  targetPath = found.path;
148
146
  }
149
147
 
150
- await withRetry(() => git.removeWorktree(projectPath, targetPath, args.force), 3, "worktree-remove");
151
- return asToolResult({ success: true, removed: targetPath });
148
+ await withRetry(() => git.removeWorktree(effectivePath, targetPath, args.force), 3, "worktree-remove");
149
+ return asToolResult({ success: true, removed: targetPath, _worktreeContext });
152
150
  }
153
151
 
154
152
  if (action === "prune") {
155
- await git.pruneWorktrees(projectPath);
156
- return asToolResult({ success: true, message: "Worktrees podados" });
153
+ await git.pruneWorktrees(effectivePath);
154
+ return asToolResult({ success: true, message: "Worktrees podados", _worktreeContext });
157
155
  }
158
156
 
159
157
  if (action === "set-channel") {
160
158
  if (!args.branch) return asToolError("MISSING_PARAMETER", "branch é obrigatório");
161
159
  if (!args.channel) return asToolError("MISSING_PARAMETER", "channel é obrigatório");
162
160
 
163
- await git.setWorktreeConfig(projectPath, args.branch, { channel: args.channel });
164
- return asToolResult({ success: true, branch: args.branch, channel: args.channel });
161
+ await git.setWorktreeConfig(effectivePath, args.branch, { channel: args.channel });
162
+ return asToolResult({ success: true, branch: args.branch, channel: args.channel, _worktreeContext });
163
+ }
164
+
165
+ if (action === "setup") {
166
+ // Setup action logic - Estrutura recomendada
167
+ // 1. Verifica se está num repo
168
+ // 2. Se não estiver, init
169
+ // 3. Move master para subpasta 'main' se estiver na raiz?
170
+ // Simplificação: Apenas cria as pastas se não existirem
171
+
172
+ const mainPath = path.join(effectivePath, 'main');
173
+ const worktreesPath = path.join(effectivePath, 'worktrees');
174
+
175
+ if (!fs.existsSync(mainPath)) {
176
+ // Se estamos na raiz de um repo normal, setup é meio redundante se não formos mover arquivos.
177
+ // Mas se o usuário quer estruturar do zero:
178
+ // git init bare? Não, o spec não diz bare.
179
+
180
+ // Vamos apenas retornar uma mensagem informativa por enquanto, pois a migração automática é arriscada.
181
+ return asToolResult({
182
+ success: true,
183
+ message: "Para configurar worktrees, recomenda-se mover o branch principal para uma pasta 'main' e criar novos worktrees em pastas irmãs.",
184
+ structure: {
185
+ root: projectPath,
186
+ recommended: ["main/", "feature-branch/", "hotfix/"]
187
+ }
188
+ });
189
+ }
165
190
  }
166
191
 
167
192
  return asToolError("VALIDATION_ERROR", `Ação '${action}' não suportada`, { availableActions: ["add", "list", "remove", "prune", "set-channel"] });