@akiojin/gwt 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.ja.md +323 -0
  2. package/README.md +347 -0
  3. package/bin/gwt.js +5 -0
  4. package/package.json +125 -0
  5. package/src/claude-history.ts +717 -0
  6. package/src/claude.ts +292 -0
  7. package/src/cli/ui/__tests__/SKIPPED_TESTS.md +119 -0
  8. package/src/cli/ui/__tests__/acceptance/branchList.acceptance.test.tsx.skip +239 -0
  9. package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +214 -0
  10. package/src/cli/ui/__tests__/acceptance/realtimeUpdate.acceptance.test.tsx.skip +219 -0
  11. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +183 -0
  12. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +313 -0
  13. package/src/cli/ui/__tests__/components/App.test.tsx +270 -0
  14. package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +66 -0
  15. package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +103 -0
  16. package/src/cli/ui/__tests__/components/common/Input.test.tsx +92 -0
  17. package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +127 -0
  18. package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +264 -0
  19. package/src/cli/ui/__tests__/components/common/Select.test.tsx +246 -0
  20. package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +62 -0
  21. package/src/cli/ui/__tests__/components/parts/Header.test.tsx +54 -0
  22. package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +68 -0
  23. package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +135 -0
  24. package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +153 -0
  25. package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +215 -0
  26. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +293 -0
  27. package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +161 -0
  28. package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +215 -0
  29. package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +99 -0
  30. package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +127 -0
  31. package/src/cli/ui/__tests__/hooks/useGitData.test.ts.skip +228 -0
  32. package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +146 -0
  33. package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +98 -0
  34. package/src/cli/ui/__tests__/integration/branchList.test.tsx.skip +253 -0
  35. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +306 -0
  36. package/src/cli/ui/__tests__/integration/navigation.test.tsx +405 -0
  37. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +505 -0
  38. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx.skip +216 -0
  39. package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +180 -0
  40. package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +237 -0
  41. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +775 -0
  42. package/src/cli/ui/__tests__/utils/statisticsCalculator.test.ts +243 -0
  43. package/src/cli/ui/components/App.tsx +793 -0
  44. package/src/cli/ui/components/common/Confirm.tsx +40 -0
  45. package/src/cli/ui/components/common/ErrorBoundary.tsx +57 -0
  46. package/src/cli/ui/components/common/Input.tsx +36 -0
  47. package/src/cli/ui/components/common/LoadingIndicator.tsx +95 -0
  48. package/src/cli/ui/components/common/Select.tsx +216 -0
  49. package/src/cli/ui/components/parts/Footer.tsx +41 -0
  50. package/src/cli/ui/components/parts/Header.test.tsx +85 -0
  51. package/src/cli/ui/components/parts/Header.tsx +63 -0
  52. package/src/cli/ui/components/parts/MergeStatusList.tsx +75 -0
  53. package/src/cli/ui/components/parts/ProgressBar.tsx +73 -0
  54. package/src/cli/ui/components/parts/ScrollableList.tsx +24 -0
  55. package/src/cli/ui/components/parts/Stats.tsx +67 -0
  56. package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +116 -0
  57. package/src/cli/ui/components/screens/BatchMergeProgressScreen.tsx +70 -0
  58. package/src/cli/ui/components/screens/BatchMergeResultScreen.tsx +104 -0
  59. package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +213 -0
  60. package/src/cli/ui/components/screens/BranchListScreen.tsx +299 -0
  61. package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +149 -0
  62. package/src/cli/ui/components/screens/PRCleanupScreen.tsx +167 -0
  63. package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +100 -0
  64. package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +117 -0
  65. package/src/cli/ui/hooks/useBatchMerge.ts +96 -0
  66. package/src/cli/ui/hooks/useGitData.ts +157 -0
  67. package/src/cli/ui/hooks/useScreenState.ts +44 -0
  68. package/src/cli/ui/hooks/useTerminalSize.ts +33 -0
  69. package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +102 -0
  70. package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +151 -0
  71. package/src/cli/ui/types.ts +295 -0
  72. package/src/cli/ui/utils/baseBranch.ts +34 -0
  73. package/src/cli/ui/utils/branchFormatter.ts +222 -0
  74. package/src/cli/ui/utils/statisticsCalculator.ts +44 -0
  75. package/src/codex.ts +139 -0
  76. package/src/config/builtin-tools.ts +44 -0
  77. package/src/config/constants.ts +100 -0
  78. package/src/config/env-history.ts +45 -0
  79. package/src/config/index.ts +204 -0
  80. package/src/config/tools.ts +293 -0
  81. package/src/git.ts +1102 -0
  82. package/src/github.ts +158 -0
  83. package/src/index.test.ts +87 -0
  84. package/src/index.ts +684 -0
  85. package/src/index.ts.backup +1543 -0
  86. package/src/launcher.ts +142 -0
  87. package/src/repositories/git.repository.ts +129 -0
  88. package/src/repositories/github.repository.ts +83 -0
  89. package/src/repositories/worktree.repository.ts +69 -0
  90. package/src/services/BatchMergeService.ts +251 -0
  91. package/src/services/WorktreeOrchestrator.ts +115 -0
  92. package/src/services/__tests__/BatchMergeService.test.ts +518 -0
  93. package/src/services/__tests__/WorktreeOrchestrator.test.ts +258 -0
  94. package/src/services/dependency-installer.ts +199 -0
  95. package/src/services/git.service.ts +113 -0
  96. package/src/services/github.service.ts +61 -0
  97. package/src/services/worktree.service.ts +66 -0
  98. package/src/types/api.ts +241 -0
  99. package/src/types/tools.ts +235 -0
  100. package/src/utils/spinner.ts +54 -0
  101. package/src/utils/terminal.ts +272 -0
  102. package/src/utils.test.ts +43 -0
  103. package/src/utils.ts +60 -0
  104. package/src/web/client/index.html +12 -0
  105. package/src/web/client/src/components/BranchGraph.tsx +231 -0
  106. package/src/web/client/src/components/EnvEditor.tsx +145 -0
  107. package/src/web/client/src/components/Terminal.tsx +137 -0
  108. package/src/web/client/src/hooks/useBranches.ts +41 -0
  109. package/src/web/client/src/hooks/useConfig.ts +31 -0
  110. package/src/web/client/src/hooks/useSessions.ts +59 -0
  111. package/src/web/client/src/hooks/useWorktrees.ts +47 -0
  112. package/src/web/client/src/index.css +834 -0
  113. package/src/web/client/src/lib/api.ts +184 -0
  114. package/src/web/client/src/lib/websocket.ts +174 -0
  115. package/src/web/client/src/main.tsx +29 -0
  116. package/src/web/client/src/pages/BranchDetailPage.tsx +847 -0
  117. package/src/web/client/src/pages/BranchListPage.tsx +264 -0
  118. package/src/web/client/src/pages/ConfigManagementPage.tsx +203 -0
  119. package/src/web/client/src/router.tsx +27 -0
  120. package/src/web/client/vite.config.ts +21 -0
  121. package/src/web/server/env/importer.ts +54 -0
  122. package/src/web/server/index.ts +74 -0
  123. package/src/web/server/pty/manager.ts +189 -0
  124. package/src/web/server/routes/branches.ts +126 -0
  125. package/src/web/server/routes/config.ts +220 -0
  126. package/src/web/server/routes/index.ts +37 -0
  127. package/src/web/server/routes/sessions.ts +130 -0
  128. package/src/web/server/routes/worktrees.ts +108 -0
  129. package/src/web/server/services/branches.ts +368 -0
  130. package/src/web/server/services/worktrees.ts +85 -0
  131. package/src/web/server/websocket/handler.ts +180 -0
  132. package/src/worktree.ts +703 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Worktree Routes
3
+ *
4
+ * Worktree関連のREST APIエンドポイント。
5
+ */
6
+
7
+ import type { FastifyInstance } from "fastify";
8
+ import {
9
+ listWorktrees,
10
+ getWorktreeByPath,
11
+ createNewWorktree,
12
+ removeWorktree,
13
+ } from "../services/worktrees.js";
14
+ import type {
15
+ ApiResponse,
16
+ Worktree,
17
+ CreateWorktreeRequest,
18
+ } from "../../../types/api.js";
19
+
20
+ /**
21
+ * Worktree関連のルートを登録
22
+ */
23
+ export async function registerWorktreeRoutes(
24
+ fastify: FastifyInstance,
25
+ ): Promise<void> {
26
+ // GET /api/worktrees - すべてのWorktree一覧を取得
27
+ fastify.get<{ Reply: ApiResponse<Worktree[]> }>(
28
+ "/api/worktrees",
29
+ async (request, reply) => {
30
+ try {
31
+ const worktrees = await listWorktrees();
32
+ return { success: true, data: worktrees };
33
+ } catch (error) {
34
+ const errorMsg = error instanceof Error ? error.message : String(error);
35
+ reply.code(500);
36
+ return {
37
+ success: false,
38
+ error: "Failed to fetch worktrees",
39
+ details: errorMsg,
40
+ };
41
+ }
42
+ },
43
+ );
44
+
45
+ // POST /api/worktrees - 新しいWorktreeを作成
46
+ fastify.post<{
47
+ Body: CreateWorktreeRequest;
48
+ Reply: ApiResponse<Worktree>;
49
+ }>("/api/worktrees", async (request, reply) => {
50
+ try {
51
+ const { branchName, createBranch = false } = request.body;
52
+
53
+ const worktree = await createNewWorktree(branchName, createBranch);
54
+ reply.code(201);
55
+ return { success: true, data: worktree };
56
+ } catch (error) {
57
+ const errorMsg = error instanceof Error ? error.message : String(error);
58
+ reply.code(500);
59
+ return {
60
+ success: false,
61
+ error: "Failed to create worktree",
62
+ details: errorMsg,
63
+ };
64
+ }
65
+ });
66
+
67
+ // DELETE /api/worktrees - Worktreeを削除
68
+ fastify.delete<{
69
+ Querystring: { path: string };
70
+ Reply:
71
+ | { success: true }
72
+ | { success: false; error: string; details?: string | null };
73
+ }>("/api/worktrees", async (request, reply) => {
74
+ try {
75
+ const { path } = request.query;
76
+
77
+ if (!path) {
78
+ reply.code(400);
79
+ return {
80
+ success: false,
81
+ error: "Missing required parameter: path",
82
+ details: null,
83
+ };
84
+ }
85
+
86
+ const worktree = await getWorktreeByPath(path);
87
+ if (!worktree) {
88
+ reply.code(404);
89
+ return {
90
+ success: false,
91
+ error: "Worktree not found",
92
+ details: `Worktree at ${path} does not exist`,
93
+ };
94
+ }
95
+
96
+ await removeWorktree(path);
97
+ return { success: true };
98
+ } catch (error) {
99
+ const errorMsg = error instanceof Error ? error.message : String(error);
100
+ reply.code(500);
101
+ return {
102
+ success: false,
103
+ error: "Failed to delete worktree",
104
+ details: errorMsg,
105
+ };
106
+ }
107
+ });
108
+ }
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Branch Service
3
+ *
4
+ * ブランチ一覧取得とマージステータス判定のビジネスロジック。
5
+ * 既存のgit.tsとgithub.tsの機能を活用します。
6
+ */
7
+
8
+ import { execa } from "execa";
9
+ import {
10
+ getAllBranches,
11
+ getRepositoryRoot,
12
+ getBranchDivergenceStatuses,
13
+ fetchAllRemotes,
14
+ pullFastForward,
15
+ } from "../../../git.js";
16
+ import { getPullRequestByBranch } from "../../../github.js";
17
+ import { listAdditionalWorktrees } from "../../../worktree.js";
18
+ import type { Branch, BranchSyncResult } from "../../../types/api.js";
19
+
20
+ type DivergenceStatus = { remoteAhead: number; localAhead: number };
21
+ type DivergenceValue = NonNullable<NonNullable<Branch["divergence"]>>;
22
+ import type { BranchInfo, PullRequest } from "../../../cli/ui/types.js";
23
+
24
+ const DEFAULT_BASE_BRANCHES = ["main", "master", "develop", "dev"];
25
+
26
+ function mapPullRequestState(state: string): "open" | "merged" | "closed" {
27
+ if (state === "OPEN") {
28
+ return "open";
29
+ }
30
+ if (state === "MERGED") {
31
+ return "merged";
32
+ }
33
+ return "closed";
34
+ }
35
+
36
+ /**
37
+ * すべてのブランチ一覧を取得(マージステータスとWorktree情報付き)
38
+ */
39
+ export async function listBranches(): Promise<Branch[]> {
40
+ const [branches, worktrees, repoRoot] = await Promise.all([
41
+ getAllBranches(),
42
+ listAdditionalWorktrees(),
43
+ getRepositoryRoot(),
44
+ ]);
45
+ const divergenceMap = await buildDivergenceMap(branches, repoRoot);
46
+ const upstreamMap = await collectUpstreamMap(repoRoot);
47
+ const baseCandidates = buildBaseBranchCandidates(branches);
48
+ const mainBranch = "main"; // TODO: 動的に取得
49
+ const refCache = new Map<string, boolean>();
50
+ const ancestorCache = new Map<string, boolean>();
51
+
52
+ const branchList: Branch[] = await Promise.all(
53
+ branches.map(async (branchInfo: BranchInfo) => {
54
+ // Worktree情報
55
+ const worktree = worktrees.find((wt) => wt.branch === branchInfo.name);
56
+
57
+ // マージステータス判定(GitHub PRベース)
58
+ let mergeStatus: "unmerged" | "merged" | "unknown" = "unknown";
59
+ const pr = await getPullRequestByBranch(branchInfo.name);
60
+ if (pr) {
61
+ mergeStatus = pr.state === "MERGED" ? "merged" : "unmerged";
62
+ } else if (branchInfo.name === mainBranch) {
63
+ // メインブランチは常にmerged扱い
64
+ mergeStatus = "merged";
65
+ }
66
+
67
+ const baseBranch = await resolveBaseBranch(
68
+ branchInfo,
69
+ pr,
70
+ upstreamMap,
71
+ baseCandidates,
72
+ repoRoot,
73
+ refCache,
74
+ ancestorCache,
75
+ );
76
+
77
+ const prInfo = pr
78
+ ? {
79
+ number: pr.number,
80
+ title: pr.title,
81
+ state: mapPullRequestState(pr.state),
82
+ mergedAt: pr.mergedAt,
83
+ }
84
+ : null;
85
+
86
+ const divergenceStatus =
87
+ branchInfo.type === "local"
88
+ ? divergenceMap.get(branchInfo.name)
89
+ : undefined;
90
+
91
+ const divergence = divergenceStatus
92
+ ? mapDivergence(divergenceStatus)
93
+ : null;
94
+
95
+ return {
96
+ name: branchInfo.name,
97
+ type: branchInfo.type,
98
+ commitHash: "unknown", // BranchInfoには含まれていない
99
+ commitMessage: null,
100
+ author: null,
101
+ commitDate: null,
102
+ mergeStatus,
103
+ hasUnpushedCommits: Boolean(branchInfo.hasUnpushedCommits),
104
+ worktreePath: worktree?.path || null,
105
+ baseBranch: baseBranch ?? null,
106
+ divergence,
107
+ prInfo,
108
+ };
109
+ }),
110
+ );
111
+
112
+ return branchList;
113
+ }
114
+
115
+ export async function syncBranchState(
116
+ branchName: string,
117
+ worktreePath: string,
118
+ ): Promise<BranchSyncResult> {
119
+ if (!worktreePath) {
120
+ throw new Error("Worktree path is required to sync branch");
121
+ }
122
+
123
+ const repoRoot = await getRepositoryRoot();
124
+ const warnings: string[] = [];
125
+
126
+ await fetchAllRemotes({ cwd: repoRoot });
127
+
128
+ let pullStatus: "success" | "failed" = "success";
129
+ try {
130
+ await pullFastForward(worktreePath);
131
+ } catch (error) {
132
+ pullStatus = "failed";
133
+ const reason = error instanceof Error ? error.message : String(error);
134
+ warnings.push(reason);
135
+ }
136
+
137
+ const sanitizedBranch = sanitizeBranchName(branchName);
138
+ const divergenceInput: { cwd: string; branches?: string[] } = {
139
+ cwd: repoRoot,
140
+ };
141
+ if (sanitizedBranch) {
142
+ divergenceInput.branches = [sanitizedBranch];
143
+ }
144
+ const divergenceStatuses = await getBranchDivergenceStatuses(divergenceInput);
145
+ const divergence =
146
+ divergenceStatuses.length > 0
147
+ ? mapDivergence(divergenceStatuses[0]!)
148
+ : null;
149
+
150
+ const branch = await getBranchByName(branchName);
151
+ if (!branch) {
152
+ throw new Error(`Branch not found: ${branchName}`);
153
+ }
154
+
155
+ const result: BranchSyncResult = {
156
+ branch,
157
+ divergence: divergence ?? branch.divergence ?? null,
158
+ fetchStatus: "success",
159
+ pullStatus,
160
+ };
161
+
162
+ if (warnings.length) {
163
+ result.warnings = warnings;
164
+ }
165
+
166
+ return result;
167
+ }
168
+
169
+ async function buildDivergenceMap(
170
+ branches: BranchInfo[],
171
+ repoRoot: string,
172
+ ): Promise<Map<string, DivergenceStatus>> {
173
+ const localBranchNames = branches
174
+ .filter((branch) => branch.type === "local")
175
+ .map((branch) => branch.name);
176
+
177
+ if (localBranchNames.length === 0) {
178
+ return new Map();
179
+ }
180
+
181
+ try {
182
+ const statuses = await getBranchDivergenceStatuses({
183
+ cwd: repoRoot,
184
+ branches: localBranchNames,
185
+ });
186
+ return new Map(statuses.map((status) => [status.branch, status]));
187
+ } catch (error) {
188
+ console.warn("Failed to compute branch divergence for Web UI", error);
189
+ return new Map();
190
+ }
191
+ }
192
+
193
+ function mapDivergence(status: DivergenceStatus): DivergenceValue {
194
+ return {
195
+ ahead: status.localAhead,
196
+ behind: status.remoteAhead,
197
+ upToDate: status.localAhead === 0 && status.remoteAhead === 0,
198
+ };
199
+ }
200
+
201
+ function sanitizeBranchName(value: string): string {
202
+ return value.replace(/^origin\//, "");
203
+ }
204
+
205
+ /**
206
+ * 特定のブランチ情報を取得
207
+ */
208
+ export async function getBranchByName(
209
+ branchName: string,
210
+ ): Promise<Branch | null> {
211
+ const branches = await listBranches();
212
+ return branches.find((b) => b.name === branchName) || null;
213
+ }
214
+
215
+ async function collectUpstreamMap(
216
+ repoRoot: string,
217
+ ): Promise<Map<string, string>> {
218
+ try {
219
+ const { stdout } = await execa(
220
+ "git",
221
+ [
222
+ "for-each-ref",
223
+ "--format=%(refname:short)|%(upstream:short)",
224
+ "refs/heads",
225
+ ],
226
+ { cwd: repoRoot },
227
+ );
228
+
229
+ return stdout
230
+ .split("\n")
231
+ .filter((line) => line.includes("|"))
232
+ .reduce((map, line) => {
233
+ const [branch, upstream] = line.split("|");
234
+ if (branch && upstream) {
235
+ map.set(branch.trim(), upstream.trim());
236
+ }
237
+ return map;
238
+ }, new Map<string, string>());
239
+ } catch {
240
+ return new Map();
241
+ }
242
+ }
243
+
244
+ function buildBaseBranchCandidates(branches: BranchInfo[]): string[] {
245
+ const candidates = new Set<string>();
246
+ for (const base of DEFAULT_BASE_BRANCHES) {
247
+ candidates.add(base);
248
+ candidates.add(`origin/${base}`);
249
+ }
250
+
251
+ for (const branch of branches) {
252
+ if (branch.branchType === "main" || branch.branchType === "develop") {
253
+ candidates.add(branch.name);
254
+ if (branch.name.startsWith("origin/")) {
255
+ const localName = branch.name.replace(/^origin\//, "");
256
+ if (localName) {
257
+ candidates.add(localName);
258
+ }
259
+ } else {
260
+ candidates.add(`origin/${branch.name}`);
261
+ }
262
+ }
263
+ }
264
+
265
+ return Array.from(candidates);
266
+ }
267
+
268
+ async function resolveBaseBranch(
269
+ branchInfo: BranchInfo,
270
+ pr: PullRequest | null,
271
+ upstreamMap: Map<string, string>,
272
+ baseCandidates: string[],
273
+ repoRoot: string,
274
+ refCache: Map<string, boolean>,
275
+ ancestorCache: Map<string, boolean>,
276
+ ): Promise<string | null> {
277
+ if (pr?.baseRefName) {
278
+ return pr.baseRefName;
279
+ }
280
+
281
+ if (branchInfo.type === "local") {
282
+ const upstream = upstreamMap.get(branchInfo.name);
283
+ if (upstream) {
284
+ return upstream;
285
+ }
286
+ }
287
+
288
+ return inferBaseBranchFromCandidates(
289
+ branchInfo.name,
290
+ baseCandidates,
291
+ repoRoot,
292
+ refCache,
293
+ ancestorCache,
294
+ );
295
+ }
296
+
297
+ async function inferBaseBranchFromCandidates(
298
+ branchName: string,
299
+ baseCandidates: string[],
300
+ repoRoot: string,
301
+ refCache: Map<string, boolean>,
302
+ ancestorCache: Map<string, boolean>,
303
+ ): Promise<string | null> {
304
+ for (const candidate of baseCandidates) {
305
+ if (!candidate || candidate === branchName) {
306
+ continue;
307
+ }
308
+
309
+ const exists = await refExists(candidate, repoRoot, refCache);
310
+ if (!exists) {
311
+ continue;
312
+ }
313
+
314
+ const isAncestor = await isAncestorRef(
315
+ candidate,
316
+ branchName,
317
+ repoRoot,
318
+ ancestorCache,
319
+ );
320
+ if (isAncestor) {
321
+ return candidate;
322
+ }
323
+ }
324
+
325
+ return null;
326
+ }
327
+
328
+ async function refExists(
329
+ refName: string,
330
+ repoRoot: string,
331
+ cache: Map<string, boolean>,
332
+ ): Promise<boolean> {
333
+ if (cache.has(refName)) {
334
+ return cache.get(refName)!;
335
+ }
336
+
337
+ try {
338
+ await execa("git", ["rev-parse", "--verify", refName], { cwd: repoRoot });
339
+ cache.set(refName, true);
340
+ return true;
341
+ } catch {
342
+ cache.set(refName, false);
343
+ return false;
344
+ }
345
+ }
346
+
347
+ async function isAncestorRef(
348
+ ancestor: string,
349
+ branch: string,
350
+ repoRoot: string,
351
+ cache: Map<string, boolean>,
352
+ ): Promise<boolean> {
353
+ const cacheKey = `${ancestor}->${branch}`;
354
+ if (cache.has(cacheKey)) {
355
+ return cache.get(cacheKey)!;
356
+ }
357
+
358
+ try {
359
+ await execa("git", ["merge-base", "--is-ancestor", ancestor, branch], {
360
+ cwd: repoRoot,
361
+ });
362
+ cache.set(cacheKey, true);
363
+ return true;
364
+ } catch {
365
+ cache.set(cacheKey, false);
366
+ return false;
367
+ }
368
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Worktree Service
3
+ *
4
+ * Worktree管理のビジネスロジック。
5
+ * 既存のworktree.tsの機能を活用します。
6
+ */
7
+
8
+ import {
9
+ listAdditionalWorktrees,
10
+ createWorktree as createWorktreeCore,
11
+ removeWorktree as removeWorktreeCore,
12
+ generateWorktreePath,
13
+ isProtectedBranchName,
14
+ type WorktreeInfo,
15
+ } from "../../../worktree.js";
16
+ import type { Worktree } from "../../../types/api.js";
17
+
18
+ /**
19
+ * すべてのWorktree一覧を取得
20
+ */
21
+ export async function listWorktrees(): Promise<Worktree[]> {
22
+ const worktrees = await listAdditionalWorktrees();
23
+
24
+ return worktrees.map((wt: WorktreeInfo) => ({
25
+ path: wt.path,
26
+ branchName: wt.branch,
27
+ head: wt.head,
28
+ isLocked: false, // TODO: locked情報を取得
29
+ isPrunable: false, // TODO: prunable情報を取得
30
+ isProtected: isProtectedBranchName(wt.branch),
31
+ createdAt: null, // git worktreeからは取得不可
32
+ lastAccessedAt: null, // git worktreeからは取得不可
33
+ }));
34
+ }
35
+
36
+ /**
37
+ * 特定のWorktree情報を取得
38
+ */
39
+ export async function getWorktreeByPath(
40
+ path: string,
41
+ ): Promise<Worktree | null> {
42
+ const worktrees = await listWorktrees();
43
+ return worktrees.find((wt) => wt.path === path) || null;
44
+ }
45
+
46
+ /**
47
+ * 新しいWorktreeを作成
48
+ */
49
+ export async function createNewWorktree(
50
+ branchName: string,
51
+ createBranch: boolean,
52
+ ): Promise<Worktree> {
53
+ const { getRepositoryRoot, getCurrentBranch } = await import(
54
+ "../../../git.js"
55
+ );
56
+
57
+ const [repoRoot, currentBranch] = await Promise.all([
58
+ getRepositoryRoot(),
59
+ getCurrentBranch(),
60
+ ]);
61
+
62
+ const worktreePath = await generateWorktreePath(branchName, repoRoot);
63
+
64
+ await createWorktreeCore({
65
+ branchName,
66
+ worktreePath,
67
+ repoRoot,
68
+ isNewBranch: createBranch,
69
+ baseBranch: currentBranch || "main",
70
+ });
71
+
72
+ const worktree = await getWorktreeByPath(worktreePath);
73
+ if (!worktree) {
74
+ throw new Error(`Failed to create worktree for branch ${branchName}`);
75
+ }
76
+
77
+ return worktree;
78
+ }
79
+
80
+ /**
81
+ * Worktreeを削除
82
+ */
83
+ export async function removeWorktree(path: string): Promise<void> {
84
+ await removeWorktreeCore(path);
85
+ }