@arii/boomtick-mcp 0.7.0 → 0.7.1

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/src/lib/git.ts CHANGED
@@ -2,6 +2,7 @@ import { runCommand } from "./shell.js";
2
2
  import { config } from "../config.js";
3
3
  import path from "path";
4
4
  import fs from "fs/promises";
5
+ import * as syncFs from "fs";
5
6
  import { execSync } from "child_process";
6
7
 
7
8
  const repoCache = new Map<string, string>();
@@ -37,6 +38,87 @@ export function resolveRepo(explicitRepo?: string | null, cwd?: string): string
37
38
  return fallback;
38
39
  }
39
40
 
41
+ export interface RepoPathOptions {
42
+ path?: string | null;
43
+ repoPath?: string | null;
44
+ repo?: string | null;
45
+ cwd?: string | null;
46
+ }
47
+
48
+ export function resolveRepoPath(options: RepoPathOptions = {}): string {
49
+ // 1. Explicit path parameter has highest precedence
50
+ if (options.path && options.path.trim().length > 0) {
51
+ return path.resolve(options.path.trim());
52
+ }
53
+
54
+ // 2. repoPath alias parameter
55
+ if (options.repoPath && options.repoPath.trim().length > 0) {
56
+ return path.resolve(options.repoPath.trim());
57
+ }
58
+
59
+ // 3. Repository name override (e.g. "arii/tech-dancer" or "tech-dancer")
60
+ if (options.repo && options.repo.trim().length > 0) {
61
+ const rawRepo = options.repo.trim();
62
+ const repoName = rawRepo.includes("/") ? rawRepo.split("/")[1] : rawRepo;
63
+
64
+ // First check project_config.json ecosystem_repos if available
65
+ let rootPath: string = process.cwd();
66
+ try {
67
+ if (options.cwd && options.cwd.trim().length > 0) {
68
+ rootPath = options.cwd.trim();
69
+ } else if (config.repoPath) {
70
+ rootPath = config.repoPath;
71
+ }
72
+ } catch {
73
+ rootPath = process.cwd();
74
+ }
75
+
76
+ const projectConfigPath = path.join(rootPath, "project_config.json");
77
+ if (syncFs.existsSync(projectConfigPath)) {
78
+ try {
79
+ const parsed = JSON.parse(syncFs.readFileSync(projectConfigPath, "utf-8"));
80
+ if (parsed?.ecosystem_repos && typeof parsed.ecosystem_repos === "object") {
81
+ for (const [key, relPath] of Object.entries(parsed.ecosystem_repos)) {
82
+ const keyRepoName = key.includes("/") ? key.split("/")[1] : key;
83
+ if (key === rawRepo || keyRepoName === repoName) {
84
+ return path.resolve(rootPath, relPath as string);
85
+ }
86
+ }
87
+ }
88
+ } catch {
89
+ // Fallback to directory inspection
90
+ }
91
+ }
92
+
93
+ // Check sibling directory relative to cwd or rootPath
94
+ const candidateDirs = [
95
+ path.resolve(process.cwd(), "..", repoName),
96
+ path.resolve(rootPath, "..", repoName),
97
+ ];
98
+ for (const cand of candidateDirs) {
99
+ if (syncFs.existsSync(cand)) {
100
+ return cand;
101
+ }
102
+ }
103
+ }
104
+
105
+ // 4. Explicit cwd parameter
106
+ if (options.cwd && options.cwd.trim().length > 0) {
107
+ return path.resolve(options.cwd.trim());
108
+ }
109
+
110
+ // 5. Configured repoPath or process.cwd()
111
+ try {
112
+ if (config.repoPath && syncFs.existsSync(config.repoPath)) {
113
+ return config.repoPath;
114
+ }
115
+ } catch {
116
+ // Fall back to process.cwd()
117
+ }
118
+
119
+ return process.cwd();
120
+ }
121
+
40
122
  function validatePrNumber(value: unknown): number {
41
123
  const prNumber = Number(value);
42
124
 
@@ -47,20 +129,25 @@ function validatePrNumber(value: unknown): number {
47
129
  return prNumber;
48
130
  }
49
131
 
50
- export async function createWorktree(branch: string, prNumber: number): Promise<string> {
132
+ export async function createWorktree(branch: string, prNumber: number, baseRepoPath?: string): Promise<string> {
51
133
  const safePrNumber = validatePrNumber(prNumber);
52
134
 
53
135
  const workspaceRoot = "/tmp/boomtick-worktrees";
54
136
  // nosemgrep
55
137
  const worktreePath = path.join(workspaceRoot, `boomtick-mcp-rescue-${safePrNumber}`);
138
+ const cwdOption = baseRepoPath ? { cwd: baseRepoPath } : undefined;
56
139
 
57
140
  // Clean up if exists
58
141
  try {
59
142
  await fs.rm(worktreePath, { recursive: true, force: true });
60
- await runCommand("git", ["worktree", "prune"]);
143
+ await runCommand("git", ["worktree", "prune"], cwdOption);
61
144
  } catch (e) { /* ignore if no previous worktree */ }
62
145
 
63
- const result = await runCommand("git", ["worktree", "add", "-b", `repair-pr-${prNumber}-${Date.now()}`, worktreePath, branch]);
146
+ const result = await runCommand(
147
+ "git",
148
+ ["worktree", "add", "-b", `repair-pr-${prNumber}-${Date.now()}`, worktreePath, branch],
149
+ cwdOption
150
+ );
64
151
 
65
152
  if (result.exitCode !== 0) {
66
153
  throw new Error(`Failed to create worktree: ${result.stderr}`);
@@ -69,9 +156,10 @@ export async function createWorktree(branch: string, prNumber: number): Promise<
69
156
  return worktreePath;
70
157
  }
71
158
 
72
- export async function removeWorktree(worktreePath: string): Promise<void> {
73
- await runCommand("git", ["worktree", "remove", "--force", worktreePath]);
74
- await runCommand("git", ["worktree", "prune"]);
159
+ export async function removeWorktree(worktreePath: string, baseRepoPath?: string): Promise<void> {
160
+ const cwdOption = baseRepoPath ? { cwd: baseRepoPath } : undefined;
161
+ await runCommand("git", ["worktree", "remove", "--force", worktreePath], cwdOption);
162
+ await runCommand("git", ["worktree", "prune"], cwdOption);
75
163
  }
76
164
 
77
165
  export interface ChangedFile {
@@ -1451,7 +1451,7 @@ export const CommitPatchResponseJsonSchema = {
1451
1451
  "type": "object"
1452
1452
  } as const;
1453
1453
 
1454
- export const CreateBranchInputSchema = z.object({ "branchName": z.string().describe("The name of the new branch"), "baseBranch": z.string().describe("Branch to branch off from").default("main") });
1454
+ export const CreateBranchInputSchema = z.object({ "branchName": z.string().describe("The name of the new branch"), "baseBranch": z.string().describe("Branch to branch off from").default("main"), "repo": z.union([z.string(), z.null()]).describe("Target repository override (e.g. org/repo or repo).").default(null), "path": z.union([z.string(), z.null()]).describe("Local filesystem path to target repository root.").default(null), "repoPath": z.union([z.string(), z.null()]).describe("Alias for path to target repository root.").default(null) });
1455
1455
 
1456
1456
  export type CreateBranchInput = z.infer<typeof CreateBranchInputSchema>;
1457
1457
 
@@ -1467,6 +1467,45 @@ export const CreateBranchInputJsonSchema = {
1467
1467
  "description": "Branch to branch off from",
1468
1468
  "title": "Basebranch",
1469
1469
  "type": "string"
1470
+ },
1471
+ "repo": {
1472
+ "anyOf": [
1473
+ {
1474
+ "type": "string"
1475
+ },
1476
+ {
1477
+ "type": "null"
1478
+ }
1479
+ ],
1480
+ "default": null,
1481
+ "description": "Target repository override (e.g. org/repo or repo).",
1482
+ "title": "Repo"
1483
+ },
1484
+ "path": {
1485
+ "anyOf": [
1486
+ {
1487
+ "type": "string"
1488
+ },
1489
+ {
1490
+ "type": "null"
1491
+ }
1492
+ ],
1493
+ "default": null,
1494
+ "description": "Local filesystem path to target repository root.",
1495
+ "title": "Path"
1496
+ },
1497
+ "repoPath": {
1498
+ "anyOf": [
1499
+ {
1500
+ "type": "string"
1501
+ },
1502
+ {
1503
+ "type": "null"
1504
+ }
1505
+ ],
1506
+ "default": null,
1507
+ "description": "Alias for path to target repository root.",
1508
+ "title": "Repopath"
1470
1509
  }
1471
1510
  },
1472
1511
  "required": [
@@ -1476,7 +1515,7 @@ export const CreateBranchInputJsonSchema = {
1476
1515
  "type": "object"
1477
1516
  } as const;
1478
1517
 
1479
- export const CreateBranchResponseSchema = z.object({ "success": z.boolean().default(true), "branchName": z.string(), "baseBranch": z.union([z.string(), z.null()]).default(null) });
1518
+ export const CreateBranchResponseSchema = z.object({ "success": z.boolean().default(true), "branchName": z.string(), "baseBranch": z.union([z.string(), z.null()]).default(null), "repo": z.union([z.string(), z.null()]).default(null), "path": z.union([z.string(), z.null()]).default(null) });
1480
1519
 
1481
1520
  export type CreateBranchResponse = z.infer<typeof CreateBranchResponseSchema>;
1482
1521
 
@@ -1502,6 +1541,30 @@ export const CreateBranchResponseJsonSchema = {
1502
1541
  ],
1503
1542
  "default": null,
1504
1543
  "title": "Basebranch"
1544
+ },
1545
+ "repo": {
1546
+ "anyOf": [
1547
+ {
1548
+ "type": "string"
1549
+ },
1550
+ {
1551
+ "type": "null"
1552
+ }
1553
+ ],
1554
+ "default": null,
1555
+ "title": "Repo"
1556
+ },
1557
+ "path": {
1558
+ "anyOf": [
1559
+ {
1560
+ "type": "string"
1561
+ },
1562
+ {
1563
+ "type": "null"
1564
+ }
1565
+ ],
1566
+ "default": null,
1567
+ "title": "Path"
1505
1568
  }
1506
1569
  },
1507
1570
  "required": [
@@ -1788,7 +1851,7 @@ export const CreatePullRequestResponseJsonSchema = {
1788
1851
  "type": "object"
1789
1852
  } as const;
1790
1853
 
1791
- export const CreateRepairBranchInputSchema = z.object({ "prNumber": z.number().int().describe("The original pull request number to repair."), "repairBranchName": z.union([z.string(), z.null()]).describe("Optional custom name for the new repair branch.").default(null), "writeMode": z.boolean().describe("Safety gate: Must be true to perform branch creation and worktree setup.") });
1854
+ export const CreateRepairBranchInputSchema = z.object({ "prNumber": z.number().int().describe("The original pull request number to repair."), "repairBranchName": z.union([z.string(), z.null()]).describe("Optional custom name for the new repair branch.").default(null), "writeMode": z.boolean().describe("Safety gate: Must be true to perform branch creation and worktree setup."), "repo": z.union([z.string(), z.null()]).describe("Target repository override (e.g. org/repo or repo).").default(null), "path": z.union([z.string(), z.null()]).describe("Local filesystem path to target repository root.").default(null), "repoPath": z.union([z.string(), z.null()]).describe("Alias for path to target repository root.").default(null) });
1792
1855
 
1793
1856
  export type CreateRepairBranchInput = z.infer<typeof CreateRepairBranchInputSchema>;
1794
1857
 
@@ -1816,6 +1879,45 @@ export const CreateRepairBranchInputJsonSchema = {
1816
1879
  "description": "Safety gate: Must be true to perform branch creation and worktree setup.",
1817
1880
  "title": "Writemode",
1818
1881
  "type": "boolean"
1882
+ },
1883
+ "repo": {
1884
+ "anyOf": [
1885
+ {
1886
+ "type": "string"
1887
+ },
1888
+ {
1889
+ "type": "null"
1890
+ }
1891
+ ],
1892
+ "default": null,
1893
+ "description": "Target repository override (e.g. org/repo or repo).",
1894
+ "title": "Repo"
1895
+ },
1896
+ "path": {
1897
+ "anyOf": [
1898
+ {
1899
+ "type": "string"
1900
+ },
1901
+ {
1902
+ "type": "null"
1903
+ }
1904
+ ],
1905
+ "default": null,
1906
+ "description": "Local filesystem path to target repository root.",
1907
+ "title": "Path"
1908
+ },
1909
+ "repoPath": {
1910
+ "anyOf": [
1911
+ {
1912
+ "type": "string"
1913
+ },
1914
+ {
1915
+ "type": "null"
1916
+ }
1917
+ ],
1918
+ "default": null,
1919
+ "description": "Alias for path to target repository root.",
1920
+ "title": "Repopath"
1819
1921
  }
1820
1922
  },
1821
1923
  "required": [
@@ -1826,7 +1928,7 @@ export const CreateRepairBranchInputJsonSchema = {
1826
1928
  "type": "object"
1827
1929
  } as const;
1828
1930
 
1829
- export const CreateRepairBranchResponseSchema = z.object({ "prNumber": z.number().int(), "originalBranch": z.string(), "repairBranch": z.string(), "baseBranch": z.string(), "worktreePath": z.string() });
1931
+ export const CreateRepairBranchResponseSchema = z.object({ "prNumber": z.number().int(), "originalBranch": z.string(), "repairBranch": z.string(), "baseBranch": z.string(), "worktreePath": z.string(), "repo": z.union([z.string(), z.null()]).default(null), "path": z.union([z.string(), z.null()]).default(null) });
1830
1932
 
1831
1933
  export type CreateRepairBranchResponse = z.infer<typeof CreateRepairBranchResponseSchema>;
1832
1934
 
@@ -1851,6 +1953,30 @@ export const CreateRepairBranchResponseJsonSchema = {
1851
1953
  "worktreePath": {
1852
1954
  "title": "Worktreepath",
1853
1955
  "type": "string"
1956
+ },
1957
+ "repo": {
1958
+ "anyOf": [
1959
+ {
1960
+ "type": "string"
1961
+ },
1962
+ {
1963
+ "type": "null"
1964
+ }
1965
+ ],
1966
+ "default": null,
1967
+ "title": "Repo"
1968
+ },
1969
+ "path": {
1970
+ "anyOf": [
1971
+ {
1972
+ "type": "string"
1973
+ },
1974
+ {
1975
+ "type": "null"
1976
+ }
1977
+ ],
1978
+ "default": null,
1979
+ "title": "Path"
1854
1980
  }
1855
1981
  },
1856
1982
  "required": [
@@ -1864,7 +1990,7 @@ export const CreateRepairBranchResponseJsonSchema = {
1864
1990
  "type": "object"
1865
1991
  } as const;
1866
1992
 
1867
- export const GetChangedFilesInputSchema = z.object({ "base": z.string().describe("The base ref to compare from (default: 'main').").default("main"), "head": z.string().describe("The head ref to compare to (default: 'HEAD').").default("HEAD") });
1993
+ export const GetChangedFilesInputSchema = z.object({ "base": z.string().describe("The base ref to compare from (default: 'main').").default("main"), "head": z.string().describe("The head ref to compare to (default: 'HEAD').").default("HEAD"), "repo": z.union([z.string(), z.null()]).describe("Target GitHub repository override.").default(null), "path": z.union([z.string(), z.null()]).describe("Local filesystem path to target repository root.").default(null), "repoPath": z.union([z.string(), z.null()]).describe("Alias for path to target repository root.").default(null) });
1868
1994
 
1869
1995
  export type GetChangedFilesInput = z.infer<typeof GetChangedFilesInputSchema>;
1870
1996
 
@@ -1881,6 +2007,45 @@ export const GetChangedFilesInputJsonSchema = {
1881
2007
  "description": "The head ref to compare to (default: 'HEAD').",
1882
2008
  "title": "Head",
1883
2009
  "type": "string"
2010
+ },
2011
+ "repo": {
2012
+ "anyOf": [
2013
+ {
2014
+ "type": "string"
2015
+ },
2016
+ {
2017
+ "type": "null"
2018
+ }
2019
+ ],
2020
+ "default": null,
2021
+ "description": "Target GitHub repository override.",
2022
+ "title": "Repo"
2023
+ },
2024
+ "path": {
2025
+ "anyOf": [
2026
+ {
2027
+ "type": "string"
2028
+ },
2029
+ {
2030
+ "type": "null"
2031
+ }
2032
+ ],
2033
+ "default": null,
2034
+ "description": "Local filesystem path to target repository root.",
2035
+ "title": "Path"
2036
+ },
2037
+ "repoPath": {
2038
+ "anyOf": [
2039
+ {
2040
+ "type": "string"
2041
+ },
2042
+ {
2043
+ "type": "null"
2044
+ }
2045
+ ],
2046
+ "default": null,
2047
+ "description": "Alias for path to target repository root.",
2048
+ "title": "Repopath"
1884
2049
  }
1885
2050
  },
1886
2051
  "title": "GetChangedFilesInput",
@@ -1960,27 +2125,77 @@ export const GetCommandSchemaResponseJsonSchema = {
1960
2125
  "type": "object"
1961
2126
  } as const;
1962
2127
 
1963
- export const GetMergeConflictFilesInputSchema = z.object({ "prNumber": z.number().int().describe("The number of the pull request to check for conflicts."), "baseBranch": z.string().describe("The base branch to check against (default: 'main').").default("main") });
2128
+ export const GetMergeConflictFilesInputSchema = z.object({ "prNumber": z.union([z.number().int(), z.null()]).describe("The number of the pull request to check for conflicts.").default(null), "baseBranch": z.string().describe("The base branch to check against (default: 'main').").default("main"), "headRef": z.string().describe("The head ref or branch to check.").default("HEAD"), "repo": z.union([z.string(), z.null()]).describe("Target GitHub repository override.").default(null), "path": z.union([z.string(), z.null()]).describe("Local filesystem path to target repository root.").default(null), "repoPath": z.union([z.string(), z.null()]).describe("Alias for path to target repository root.").default(null) });
1964
2129
 
1965
2130
  export type GetMergeConflictFilesInput = z.infer<typeof GetMergeConflictFilesInputSchema>;
1966
2131
 
1967
2132
  export const GetMergeConflictFilesInputJsonSchema = {
1968
2133
  "properties": {
1969
2134
  "prNumber": {
2135
+ "anyOf": [
2136
+ {
2137
+ "type": "integer"
2138
+ },
2139
+ {
2140
+ "type": "null"
2141
+ }
2142
+ ],
2143
+ "default": null,
1970
2144
  "description": "The number of the pull request to check for conflicts.",
1971
- "title": "Prnumber",
1972
- "type": "integer"
2145
+ "title": "Prnumber"
1973
2146
  },
1974
2147
  "baseBranch": {
1975
2148
  "default": "main",
1976
2149
  "description": "The base branch to check against (default: 'main').",
1977
2150
  "title": "Basebranch",
1978
2151
  "type": "string"
2152
+ },
2153
+ "headRef": {
2154
+ "default": "HEAD",
2155
+ "description": "The head ref or branch to check.",
2156
+ "title": "Headref",
2157
+ "type": "string"
2158
+ },
2159
+ "repo": {
2160
+ "anyOf": [
2161
+ {
2162
+ "type": "string"
2163
+ },
2164
+ {
2165
+ "type": "null"
2166
+ }
2167
+ ],
2168
+ "default": null,
2169
+ "description": "Target GitHub repository override.",
2170
+ "title": "Repo"
2171
+ },
2172
+ "path": {
2173
+ "anyOf": [
2174
+ {
2175
+ "type": "string"
2176
+ },
2177
+ {
2178
+ "type": "null"
2179
+ }
2180
+ ],
2181
+ "default": null,
2182
+ "description": "Local filesystem path to target repository root.",
2183
+ "title": "Path"
2184
+ },
2185
+ "repoPath": {
2186
+ "anyOf": [
2187
+ {
2188
+ "type": "string"
2189
+ },
2190
+ {
2191
+ "type": "null"
2192
+ }
2193
+ ],
2194
+ "default": null,
2195
+ "description": "Alias for path to target repository root.",
2196
+ "title": "Repopath"
1979
2197
  }
1980
2198
  },
1981
- "required": [
1982
- "prNumber"
1983
- ],
1984
2199
  "title": "GetMergeConflictFilesInput",
1985
2200
  "type": "object"
1986
2201
  } as const;
@@ -1,16 +1,25 @@
1
1
  import { z } from "zod";
2
- import { getMergeConflictFiles } from "../lib/git.js";
2
+ import { getMergeConflictFiles, resolveRepoPath } from "../lib/git.js";
3
3
 
4
4
  export const GetMergeConflictFilesInputSchema = z.object({
5
5
  prNumber: z.number().optional(),
6
6
  baseBranch: z.string().optional().default("main"),
7
7
  headRef: z.string().optional().default("HEAD"),
8
+ repo: z.string().optional().describe("Target GitHub repository override."),
9
+ path: z.string().optional().describe("Local filesystem path to target repository root."),
10
+ repoPath: z.string().optional().describe("Alias for path to target repository root."),
8
11
  });
9
12
 
10
13
  export async function getMergeConflictFilesHandler(args: z.infer<typeof GetMergeConflictFilesInputSchema>) {
11
14
  const baseBranch = args.baseBranch || "main";
12
15
  const headRef = args.headRef || "HEAD";
13
- const conflictFiles = await getMergeConflictFiles(baseBranch, headRef);
16
+ const targetCwd = resolveRepoPath({
17
+ path: args.path,
18
+ repoPath: args.repoPath,
19
+ repo: args.repo,
20
+ });
21
+
22
+ const conflictFiles = await getMergeConflictFiles(baseBranch, headRef, targetCwd);
14
23
 
15
24
  const hasConflicts = conflictFiles.length > 0;
16
25
 
@@ -21,13 +21,22 @@ describe("repo.create_branch", () => {
21
21
 
22
22
  const result = await createBranchHandler(args);
23
23
 
24
- expect(shell.runCommand).toHaveBeenCalledWith("git", ["fetch", "origin", "main"]);
25
- expect(shell.runCommand).toHaveBeenCalledWith("git", ["checkout", "-b", "new-feature", "origin/main"]);
26
- expect(result).toEqual({
24
+ expect(shell.runCommand).toHaveBeenCalledWith(
25
+ "git",
26
+ ["fetch", "origin", "main"],
27
+ { cwd: expect.any(String) }
28
+ );
29
+ expect(shell.runCommand).toHaveBeenCalledWith(
30
+ "git",
31
+ ["checkout", "-b", "new-feature", "origin/main"],
32
+ { cwd: expect.any(String) }
33
+ );
34
+ expect(result).toMatchObject({
27
35
  success: true,
28
36
  branchName: "new-feature",
29
37
  baseBranch: "main",
30
38
  });
39
+ expect(result.path).toBeDefined();
31
40
  });
32
41
 
33
42
  it("should use default base branch 'main' if not provided", async () => {
@@ -39,8 +48,67 @@ describe("repo.create_branch", () => {
39
48
 
40
49
  const result = await createBranchHandler(args);
41
50
 
42
- expect(shell.runCommand).toHaveBeenCalledWith("git", ["fetch", "origin", "main"]);
43
- expect(shell.runCommand).toHaveBeenCalledWith("git", ["checkout", "-b", "another-feature", "origin/main"]);
51
+ expect(shell.runCommand).toHaveBeenCalledWith(
52
+ "git",
53
+ ["fetch", "origin", "main"],
54
+ { cwd: expect.any(String) }
55
+ );
56
+ expect(shell.runCommand).toHaveBeenCalledWith(
57
+ "git",
58
+ ["checkout", "-b", "another-feature", "origin/main"],
59
+ { cwd: expect.any(String) }
60
+ );
61
+ expect(result.success).toBe(true);
62
+ });
63
+
64
+ it("should create branch in specified target workspace path", async () => {
65
+ const args: CreateBranchInput = {
66
+ branchName: "custom-path-feature",
67
+ baseBranch: "main",
68
+ path: "/home/ari/tech-dancer",
69
+ };
70
+
71
+ (shell.runCommand as any).mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" });
72
+
73
+ const result = await createBranchHandler(args);
74
+
75
+ expect(shell.runCommand).toHaveBeenCalledWith(
76
+ "git",
77
+ ["fetch", "origin", "main"],
78
+ { cwd: "/home/ari/tech-dancer" }
79
+ );
80
+ expect(shell.runCommand).toHaveBeenCalledWith(
81
+ "git",
82
+ ["checkout", "-b", "custom-path-feature", "origin/main"],
83
+ { cwd: "/home/ari/tech-dancer" }
84
+ );
85
+ expect(result).toEqual({
86
+ success: true,
87
+ branchName: "custom-path-feature",
88
+ baseBranch: "main",
89
+ path: "/home/ari/tech-dancer",
90
+ });
91
+ });
92
+
93
+ it("should include repo field when repo override is specified", async () => {
94
+ const args: CreateBranchInput = {
95
+ branchName: "repo-feature",
96
+ baseBranch: "main",
97
+ repo: "arii/tech-dancer",
98
+ path: "/home/ari/tech-dancer",
99
+ };
100
+
101
+ (shell.runCommand as any).mockResolvedValue({ exitCode: 0, stdout: "", stderr: "" });
102
+
103
+ const result = await createBranchHandler(args);
104
+
105
+ expect(result).toEqual({
106
+ success: true,
107
+ branchName: "repo-feature",
108
+ baseBranch: "main",
109
+ path: "/home/ari/tech-dancer",
110
+ repo: "arii/tech-dancer",
111
+ });
44
112
  });
45
113
 
46
114
  it("should throw error if git command fails", async () => {
@@ -1,29 +1,55 @@
1
1
  import { z } from "zod";
2
2
  import { runCommand } from "../lib/shell.js";
3
+ import { resolveRepoPath } from "../lib/git.js";
3
4
 
4
5
  export const CreateBranchInputSchema = z.object({
5
6
  branchName: z.string(),
6
7
  baseBranch: z.string().optional().default("main"),
8
+ repo: z.string().optional().describe("Target repository override (e.g. org/repo or repo)."),
9
+ path: z.string().optional().describe("Local filesystem path to target repository root."),
10
+ repoPath: z.string().optional().describe("Alias for path to target repository root."),
7
11
  });
8
12
 
9
13
  export type CreateBranchInput = z.input<typeof CreateBranchInputSchema>;
10
14
 
11
15
  export async function createBranchHandler(args: CreateBranchInput) {
12
16
  const params = CreateBranchInputSchema.parse(args);
17
+ const targetCwd = resolveRepoPath({
18
+ path: params.path,
19
+ repoPath: params.repoPath,
20
+ repo: params.repo,
21
+ });
13
22
 
14
23
  // Ensure latest base branch
15
- await runCommand("git", ["fetch", "origin", params.baseBranch]);
24
+ await runCommand("git", ["fetch", "origin", params.baseBranch], { cwd: targetCwd });
16
25
 
17
26
  // Create branch from origin base branch
18
- const result = await runCommand("git", ["checkout", "-b", params.branchName, `origin/${params.baseBranch}`]);
27
+ const result = await runCommand(
28
+ "git",
29
+ ["checkout", "-b", params.branchName, `origin/${params.baseBranch}`],
30
+ { cwd: targetCwd }
31
+ );
19
32
 
20
33
  if (result.exitCode !== 0) {
21
34
  throw new Error(`Failed to create branch ${params.branchName}: ${result.stderr}`);
22
35
  }
23
36
 
24
- return {
37
+ const response: {
38
+ success: boolean;
39
+ branchName: string;
40
+ baseBranch: string;
41
+ path: string;
42
+ repo?: string;
43
+ } = {
25
44
  success: true,
26
45
  branchName: params.branchName,
27
- baseBranch: params.baseBranch
46
+ baseBranch: params.baseBranch,
47
+ path: targetCwd,
28
48
  };
49
+
50
+ if (params.repo) {
51
+ response.repo = params.repo;
52
+ }
53
+
54
+ return response;
29
55
  }