@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.
- package/README.ja.md +323 -0
- package/README.md +347 -0
- package/bin/gwt.js +5 -0
- package/package.json +125 -0
- package/src/claude-history.ts +717 -0
- package/src/claude.ts +292 -0
- package/src/cli/ui/__tests__/SKIPPED_TESTS.md +119 -0
- package/src/cli/ui/__tests__/acceptance/branchList.acceptance.test.tsx.skip +239 -0
- package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +214 -0
- package/src/cli/ui/__tests__/acceptance/realtimeUpdate.acceptance.test.tsx.skip +219 -0
- package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +183 -0
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +313 -0
- package/src/cli/ui/__tests__/components/App.test.tsx +270 -0
- package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +66 -0
- package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +103 -0
- package/src/cli/ui/__tests__/components/common/Input.test.tsx +92 -0
- package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +127 -0
- package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +264 -0
- package/src/cli/ui/__tests__/components/common/Select.test.tsx +246 -0
- package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +62 -0
- package/src/cli/ui/__tests__/components/parts/Header.test.tsx +54 -0
- package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +68 -0
- package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +135 -0
- package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +153 -0
- package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +215 -0
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +293 -0
- package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +161 -0
- package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +215 -0
- package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +99 -0
- package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +127 -0
- package/src/cli/ui/__tests__/hooks/useGitData.test.ts.skip +228 -0
- package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +146 -0
- package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +98 -0
- package/src/cli/ui/__tests__/integration/branchList.test.tsx.skip +253 -0
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +306 -0
- package/src/cli/ui/__tests__/integration/navigation.test.tsx +405 -0
- package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +505 -0
- package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx.skip +216 -0
- package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +180 -0
- package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +237 -0
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +775 -0
- package/src/cli/ui/__tests__/utils/statisticsCalculator.test.ts +243 -0
- package/src/cli/ui/components/App.tsx +793 -0
- package/src/cli/ui/components/common/Confirm.tsx +40 -0
- package/src/cli/ui/components/common/ErrorBoundary.tsx +57 -0
- package/src/cli/ui/components/common/Input.tsx +36 -0
- package/src/cli/ui/components/common/LoadingIndicator.tsx +95 -0
- package/src/cli/ui/components/common/Select.tsx +216 -0
- package/src/cli/ui/components/parts/Footer.tsx +41 -0
- package/src/cli/ui/components/parts/Header.test.tsx +85 -0
- package/src/cli/ui/components/parts/Header.tsx +63 -0
- package/src/cli/ui/components/parts/MergeStatusList.tsx +75 -0
- package/src/cli/ui/components/parts/ProgressBar.tsx +73 -0
- package/src/cli/ui/components/parts/ScrollableList.tsx +24 -0
- package/src/cli/ui/components/parts/Stats.tsx +67 -0
- package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +116 -0
- package/src/cli/ui/components/screens/BatchMergeProgressScreen.tsx +70 -0
- package/src/cli/ui/components/screens/BatchMergeResultScreen.tsx +104 -0
- package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +213 -0
- package/src/cli/ui/components/screens/BranchListScreen.tsx +299 -0
- package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +149 -0
- package/src/cli/ui/components/screens/PRCleanupScreen.tsx +167 -0
- package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +100 -0
- package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +117 -0
- package/src/cli/ui/hooks/useBatchMerge.ts +96 -0
- package/src/cli/ui/hooks/useGitData.ts +157 -0
- package/src/cli/ui/hooks/useScreenState.ts +44 -0
- package/src/cli/ui/hooks/useTerminalSize.ts +33 -0
- package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +102 -0
- package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +151 -0
- package/src/cli/ui/types.ts +295 -0
- package/src/cli/ui/utils/baseBranch.ts +34 -0
- package/src/cli/ui/utils/branchFormatter.ts +222 -0
- package/src/cli/ui/utils/statisticsCalculator.ts +44 -0
- package/src/codex.ts +139 -0
- package/src/config/builtin-tools.ts +44 -0
- package/src/config/constants.ts +100 -0
- package/src/config/env-history.ts +45 -0
- package/src/config/index.ts +204 -0
- package/src/config/tools.ts +293 -0
- package/src/git.ts +1102 -0
- package/src/github.ts +158 -0
- package/src/index.test.ts +87 -0
- package/src/index.ts +684 -0
- package/src/index.ts.backup +1543 -0
- package/src/launcher.ts +142 -0
- package/src/repositories/git.repository.ts +129 -0
- package/src/repositories/github.repository.ts +83 -0
- package/src/repositories/worktree.repository.ts +69 -0
- package/src/services/BatchMergeService.ts +251 -0
- package/src/services/WorktreeOrchestrator.ts +115 -0
- package/src/services/__tests__/BatchMergeService.test.ts +518 -0
- package/src/services/__tests__/WorktreeOrchestrator.test.ts +258 -0
- package/src/services/dependency-installer.ts +199 -0
- package/src/services/git.service.ts +113 -0
- package/src/services/github.service.ts +61 -0
- package/src/services/worktree.service.ts +66 -0
- package/src/types/api.ts +241 -0
- package/src/types/tools.ts +235 -0
- package/src/utils/spinner.ts +54 -0
- package/src/utils/terminal.ts +272 -0
- package/src/utils.test.ts +43 -0
- package/src/utils.ts +60 -0
- package/src/web/client/index.html +12 -0
- package/src/web/client/src/components/BranchGraph.tsx +231 -0
- package/src/web/client/src/components/EnvEditor.tsx +145 -0
- package/src/web/client/src/components/Terminal.tsx +137 -0
- package/src/web/client/src/hooks/useBranches.ts +41 -0
- package/src/web/client/src/hooks/useConfig.ts +31 -0
- package/src/web/client/src/hooks/useSessions.ts +59 -0
- package/src/web/client/src/hooks/useWorktrees.ts +47 -0
- package/src/web/client/src/index.css +834 -0
- package/src/web/client/src/lib/api.ts +184 -0
- package/src/web/client/src/lib/websocket.ts +174 -0
- package/src/web/client/src/main.tsx +29 -0
- package/src/web/client/src/pages/BranchDetailPage.tsx +847 -0
- package/src/web/client/src/pages/BranchListPage.tsx +264 -0
- package/src/web/client/src/pages/ConfigManagementPage.tsx +203 -0
- package/src/web/client/src/router.tsx +27 -0
- package/src/web/client/vite.config.ts +21 -0
- package/src/web/server/env/importer.ts +54 -0
- package/src/web/server/index.ts +74 -0
- package/src/web/server/pty/manager.ts +189 -0
- package/src/web/server/routes/branches.ts +126 -0
- package/src/web/server/routes/config.ts +220 -0
- package/src/web/server/routes/index.ts +37 -0
- package/src/web/server/routes/sessions.ts +130 -0
- package/src/web/server/routes/worktrees.ts +108 -0
- package/src/web/server/services/branches.ts +368 -0
- package/src/web/server/services/worktrees.ts +85 -0
- package/src/web/server/websocket/handler.ts +180 -0
- package/src/worktree.ts +703 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
isGitRepository,
|
|
5
|
+
getRepositoryRoot,
|
|
6
|
+
branchExists,
|
|
7
|
+
fetchAllRemotes,
|
|
8
|
+
pullFastForward,
|
|
9
|
+
getBranchDivergenceStatuses,
|
|
10
|
+
GitError,
|
|
11
|
+
} from "./git.js";
|
|
12
|
+
import { launchClaudeCode } from "./claude.js";
|
|
13
|
+
import { launchCodexCLI, CodexError } from "./codex.js";
|
|
14
|
+
import {
|
|
15
|
+
WorktreeOrchestrator,
|
|
16
|
+
type EnsureWorktreeOptions,
|
|
17
|
+
} from "./services/WorktreeOrchestrator.js";
|
|
18
|
+
import chalk from "chalk";
|
|
19
|
+
import type { SelectionResult } from "./cli/ui/components/App.js";
|
|
20
|
+
import {
|
|
21
|
+
worktreeExists,
|
|
22
|
+
isProtectedBranchName,
|
|
23
|
+
switchToProtectedBranch,
|
|
24
|
+
WorktreeError,
|
|
25
|
+
} from "./worktree.js";
|
|
26
|
+
import {
|
|
27
|
+
getTerminalStreams,
|
|
28
|
+
waitForUserAcknowledgement,
|
|
29
|
+
} from "./utils/terminal.js";
|
|
30
|
+
import { getToolById, getSharedEnvironment } from "./config/tools.js";
|
|
31
|
+
import { launchCustomAITool } from "./launcher.js";
|
|
32
|
+
import { saveSession } from "./config/index.js";
|
|
33
|
+
import { getPackageVersion } from "./utils.js";
|
|
34
|
+
import readline from "node:readline";
|
|
35
|
+
import {
|
|
36
|
+
installDependenciesForWorktree,
|
|
37
|
+
DependencyInstallError,
|
|
38
|
+
type DependencyInstallResult,
|
|
39
|
+
} from "./services/dependency-installer.js";
|
|
40
|
+
|
|
41
|
+
const ERROR_PROMPT = chalk.yellow(
|
|
42
|
+
"Review the error details, then press Enter to continue.",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
async function waitForErrorAcknowledgement(): Promise<void> {
|
|
46
|
+
await waitForUserAcknowledgement(ERROR_PROMPT);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Simple print functions (replacing legacy UI display functions)
|
|
51
|
+
*/
|
|
52
|
+
function printError(message: string): void {
|
|
53
|
+
console.error(chalk.red(`❌ ${message}`));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function printInfo(message: string): void {
|
|
57
|
+
console.log(chalk.blue(`ℹ️ ${message}`));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function printWarning(message: string): void {
|
|
61
|
+
console.warn(chalk.yellow(`⚠️ ${message}`));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type GitStepResult<T> = { ok: true; value: T } | { ok: false };
|
|
65
|
+
|
|
66
|
+
function isGitRelatedError(error: unknown): boolean {
|
|
67
|
+
if (!error) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (error instanceof GitError || error instanceof WorktreeError) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (error instanceof Error) {
|
|
76
|
+
return error.name === "GitError" || error.name === "WorktreeError";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
typeof error === "object" &&
|
|
81
|
+
"name" in (error as Record<string, unknown>)
|
|
82
|
+
) {
|
|
83
|
+
const name = (error as { name?: string }).name;
|
|
84
|
+
return name === "GitError" || name === "WorktreeError";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isRecoverableError(error: unknown): boolean {
|
|
91
|
+
if (!error) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (
|
|
96
|
+
error instanceof GitError ||
|
|
97
|
+
error instanceof WorktreeError ||
|
|
98
|
+
error instanceof CodexError ||
|
|
99
|
+
error instanceof DependencyInstallError
|
|
100
|
+
) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (error instanceof Error) {
|
|
105
|
+
return (
|
|
106
|
+
error.name === "GitError" ||
|
|
107
|
+
error.name === "WorktreeError" ||
|
|
108
|
+
error.name === "CodexError" ||
|
|
109
|
+
error.name === "DependencyInstallError"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
typeof error === "object" &&
|
|
115
|
+
"name" in (error as Record<string, unknown>)
|
|
116
|
+
) {
|
|
117
|
+
const name = (error as { name?: string }).name;
|
|
118
|
+
return (
|
|
119
|
+
name === "GitError" ||
|
|
120
|
+
name === "WorktreeError" ||
|
|
121
|
+
name === "CodexError" ||
|
|
122
|
+
name === "DependencyInstallError"
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function runGitStep<T>(
|
|
130
|
+
description: string,
|
|
131
|
+
step: () => Promise<T>,
|
|
132
|
+
): Promise<GitStepResult<T>> {
|
|
133
|
+
try {
|
|
134
|
+
const value = await step();
|
|
135
|
+
return { ok: true, value };
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (isGitRelatedError(error)) {
|
|
138
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
139
|
+
printWarning(`Git operation failed (${description}). Error: ${details}`);
|
|
140
|
+
await waitForErrorAcknowledgement();
|
|
141
|
+
return { ok: false };
|
|
142
|
+
}
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function runDependencyInstallStep<T extends DependencyInstallResult>(
|
|
148
|
+
description: string,
|
|
149
|
+
step: () => Promise<T>,
|
|
150
|
+
): Promise<{ ok: true; value: T }> {
|
|
151
|
+
try {
|
|
152
|
+
const value = await step();
|
|
153
|
+
return { ok: true, value };
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof DependencyInstallError) {
|
|
156
|
+
const details = error.message ?? "";
|
|
157
|
+
// 依存インストールが失敗してもワークフロー自体は継続させる
|
|
158
|
+
printError(`Failed to complete ${description}. ${details}`);
|
|
159
|
+
await waitForErrorAcknowledgement();
|
|
160
|
+
|
|
161
|
+
const fallbackResult = {
|
|
162
|
+
skipped: true,
|
|
163
|
+
manager: null,
|
|
164
|
+
lockfile: null,
|
|
165
|
+
reason: "unknown-error",
|
|
166
|
+
message: details,
|
|
167
|
+
} as T;
|
|
168
|
+
|
|
169
|
+
return { ok: true, value: fallbackResult };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function waitForEnter(promptMessage: string): Promise<void> {
|
|
177
|
+
if (!process.stdin.isTTY) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await new Promise<void>((resolve) => {
|
|
182
|
+
const rl = readline.createInterface({
|
|
183
|
+
input: process.stdin,
|
|
184
|
+
output: process.stdout,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
rl.question(`${promptMessage}\n`, () => {
|
|
188
|
+
rl.close();
|
|
189
|
+
resolve();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function showHelp(): void {
|
|
195
|
+
console.log(`
|
|
196
|
+
Worktree Manager
|
|
197
|
+
|
|
198
|
+
Usage: gwt [options]
|
|
199
|
+
|
|
200
|
+
Options:
|
|
201
|
+
-h, --help Show this help message
|
|
202
|
+
-v, --version Show version information
|
|
203
|
+
|
|
204
|
+
Description:
|
|
205
|
+
Interactive Git worktree manager with AI tool selection (Claude Code / Codex CLI) and graphical branch selection.
|
|
206
|
+
Launch without additional options to open the interactive menu.
|
|
207
|
+
`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Display application version
|
|
212
|
+
* Reads version from package.json and outputs to stdout
|
|
213
|
+
* Exits with code 1 if version cannot be retrieved
|
|
214
|
+
*/
|
|
215
|
+
async function showVersion(): Promise<void> {
|
|
216
|
+
const version = await getPackageVersion();
|
|
217
|
+
if (version) {
|
|
218
|
+
console.log(version);
|
|
219
|
+
} else {
|
|
220
|
+
console.error("Error: Unable to retrieve version information");
|
|
221
|
+
await waitForErrorAcknowledgement();
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Main function for Ink.js UI
|
|
228
|
+
* Returns SelectionResult if user made selections, undefined if user quit
|
|
229
|
+
*/
|
|
230
|
+
async function mainInkUI(): Promise<SelectionResult | undefined> {
|
|
231
|
+
const { render } = await import("ink");
|
|
232
|
+
const React = await import("react");
|
|
233
|
+
const { App } = await import("./cli/ui/components/App.js");
|
|
234
|
+
const terminal = getTerminalStreams();
|
|
235
|
+
|
|
236
|
+
let selectionResult: SelectionResult | undefined;
|
|
237
|
+
|
|
238
|
+
if (typeof terminal.stdin.resume === "function") {
|
|
239
|
+
terminal.stdin.resume();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const { unmount, waitUntilExit } = render(
|
|
243
|
+
React.createElement(App, {
|
|
244
|
+
onExit: (result?: SelectionResult) => {
|
|
245
|
+
selectionResult = result;
|
|
246
|
+
},
|
|
247
|
+
}),
|
|
248
|
+
{
|
|
249
|
+
stdin: terminal.stdin,
|
|
250
|
+
stdout: terminal.stdout,
|
|
251
|
+
stderr: terminal.stderr,
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// Wait for user to exit
|
|
256
|
+
try {
|
|
257
|
+
await waitUntilExit();
|
|
258
|
+
} finally {
|
|
259
|
+
terminal.exitRawMode();
|
|
260
|
+
if (typeof terminal.stdin.pause === "function") {
|
|
261
|
+
terminal.stdin.pause();
|
|
262
|
+
}
|
|
263
|
+
// Inkが残した data リスナーが子プロセス入力を奪わないようクリーンアップ
|
|
264
|
+
terminal.stdin.removeAllListeners?.("data");
|
|
265
|
+
terminal.stdin.removeAllListeners?.("keypress");
|
|
266
|
+
terminal.stdin.removeAllListeners?.("readable");
|
|
267
|
+
unmount();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return selectionResult;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Handle AI tool workflow
|
|
275
|
+
*/
|
|
276
|
+
export async function handleAIToolWorkflow(
|
|
277
|
+
selectionResult: SelectionResult,
|
|
278
|
+
): Promise<void> {
|
|
279
|
+
const {
|
|
280
|
+
branch,
|
|
281
|
+
displayName,
|
|
282
|
+
branchType,
|
|
283
|
+
remoteBranch,
|
|
284
|
+
tool,
|
|
285
|
+
mode,
|
|
286
|
+
skipPermissions,
|
|
287
|
+
} = selectionResult;
|
|
288
|
+
|
|
289
|
+
const branchLabel = displayName ?? branch;
|
|
290
|
+
printInfo(
|
|
291
|
+
`Selected: ${branchLabel} with ${tool} (${mode} mode, skipPermissions: ${skipPermissions})`,
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
// Get repository root
|
|
296
|
+
const repoRootResult = await runGitStep("retrieve repository root", () =>
|
|
297
|
+
getRepositoryRoot(),
|
|
298
|
+
);
|
|
299
|
+
if (!repoRootResult.ok) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const repoRoot = repoRootResult.value;
|
|
303
|
+
|
|
304
|
+
// Determine ensure options (local vs remote branch)
|
|
305
|
+
const ensureOptions: EnsureWorktreeOptions = {};
|
|
306
|
+
|
|
307
|
+
if (branchType === "remote") {
|
|
308
|
+
const remoteRef = remoteBranch ?? branch;
|
|
309
|
+
const localExists = await branchExists(branch);
|
|
310
|
+
|
|
311
|
+
ensureOptions.baseBranch = remoteRef;
|
|
312
|
+
ensureOptions.isNewBranch = !localExists;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const existingWorktree = await worktreeExists(branch);
|
|
316
|
+
|
|
317
|
+
const isProtectedBranch =
|
|
318
|
+
isProtectedBranchName(branch) ||
|
|
319
|
+
(remoteBranch ? isProtectedBranchName(remoteBranch) : false);
|
|
320
|
+
|
|
321
|
+
let protectedCheckoutResult: "none" | "local" | "remote" = "none";
|
|
322
|
+
if (isProtectedBranch) {
|
|
323
|
+
const protectedRemoteRef =
|
|
324
|
+
remoteBranch ??
|
|
325
|
+
(branchType === "remote" ? (displayName ?? branch) : null);
|
|
326
|
+
const switchResult = await runGitStep(
|
|
327
|
+
`check out protected branch '${branch}'`,
|
|
328
|
+
() =>
|
|
329
|
+
switchToProtectedBranch({
|
|
330
|
+
branchName: branch,
|
|
331
|
+
repoRoot,
|
|
332
|
+
remoteRef: protectedRemoteRef ?? null,
|
|
333
|
+
}),
|
|
334
|
+
);
|
|
335
|
+
if (!switchResult.ok) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
protectedCheckoutResult = switchResult.value;
|
|
339
|
+
ensureOptions.isNewBranch = false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const willCreateWorktree = !existingWorktree && !isProtectedBranch;
|
|
343
|
+
|
|
344
|
+
const orchestrator = new WorktreeOrchestrator();
|
|
345
|
+
|
|
346
|
+
// Ensure worktree exists (using orchestrator)
|
|
347
|
+
if (willCreateWorktree) {
|
|
348
|
+
const targetLabel = ensureOptions.isNewBranch
|
|
349
|
+
? `base ${ensureOptions.baseBranch ?? branch}`
|
|
350
|
+
: `branch ${branch}`;
|
|
351
|
+
printInfo(
|
|
352
|
+
`Creating worktree for ${targetLabel}. Progress indicator running...`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const worktreeResult = await runGitStep(
|
|
357
|
+
`prepare worktree (${branch})`,
|
|
358
|
+
() => orchestrator.ensureWorktree(branch, repoRoot, ensureOptions),
|
|
359
|
+
);
|
|
360
|
+
if (!worktreeResult.ok) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const worktreePath = worktreeResult.value;
|
|
364
|
+
|
|
365
|
+
if (isProtectedBranch) {
|
|
366
|
+
if (protectedCheckoutResult === "remote" && remoteBranch) {
|
|
367
|
+
printInfo(
|
|
368
|
+
`Created local tracking branch '${branch}' from ${remoteBranch} in repository root.`,
|
|
369
|
+
);
|
|
370
|
+
} else if (protectedCheckoutResult === "local") {
|
|
371
|
+
printInfo(
|
|
372
|
+
`Checked out protected branch '${branch}' in repository root.`,
|
|
373
|
+
);
|
|
374
|
+
} else {
|
|
375
|
+
printInfo(`Using repository root for protected branch '${branch}'.`);
|
|
376
|
+
}
|
|
377
|
+
} else if (existingWorktree) {
|
|
378
|
+
printInfo(`Reusing existing worktree: ${existingWorktree}`);
|
|
379
|
+
} else if (ensureOptions.isNewBranch) {
|
|
380
|
+
const base = ensureOptions.baseBranch ?? "";
|
|
381
|
+
printInfo(`Created new worktree from ${base}: ${worktreePath}`);
|
|
382
|
+
} else if (willCreateWorktree) {
|
|
383
|
+
printInfo(`Created worktree: ${worktreePath}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
printInfo(`Worktree ready: ${worktreePath}`);
|
|
387
|
+
|
|
388
|
+
const dependencyResult = await runDependencyInstallStep(
|
|
389
|
+
`dependency installation (${branch})`,
|
|
390
|
+
() => installDependenciesForWorktree(worktreePath),
|
|
391
|
+
);
|
|
392
|
+
if (!dependencyResult.ok) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const dependencyStatus = dependencyResult.value;
|
|
396
|
+
|
|
397
|
+
if (dependencyStatus.skipped) {
|
|
398
|
+
let warningMessage: string;
|
|
399
|
+
switch (dependencyStatus.reason) {
|
|
400
|
+
case "missing-lockfile":
|
|
401
|
+
warningMessage =
|
|
402
|
+
"Skipping automatic install because no lockfiles (bun.lock / pnpm-lock.yaml / package-lock.json) or package.json were found. Run the appropriate package-manager install command manually if needed.";
|
|
403
|
+
break;
|
|
404
|
+
case "missing-binary":
|
|
405
|
+
warningMessage = `Package manager '${dependencyStatus.manager ?? "unknown"}' is not available in this environment; skipping automatic install.`;
|
|
406
|
+
break;
|
|
407
|
+
case "install-failed":
|
|
408
|
+
warningMessage = `Dependency installation failed via ${dependencyStatus.manager ?? "unknown"}. Continuing without reinstall.`;
|
|
409
|
+
break;
|
|
410
|
+
case "lockfile-access-error":
|
|
411
|
+
warningMessage =
|
|
412
|
+
"Unable to read dependency lockfiles due to a filesystem error. Continuing without reinstall.";
|
|
413
|
+
break;
|
|
414
|
+
default:
|
|
415
|
+
warningMessage =
|
|
416
|
+
"Skipping automatic dependency install due to an unexpected error. Continuing without reinstall.";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (dependencyStatus.message) {
|
|
420
|
+
warningMessage = `${warningMessage}\nDetails: ${dependencyStatus.message}`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
printWarning(warningMessage);
|
|
424
|
+
} else {
|
|
425
|
+
printInfo(`Dependencies synced via ${dependencyStatus.manager}.`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Update remotes and attempt fast-forward pull
|
|
429
|
+
const fetchResult = await runGitStep("fetch remotes", () =>
|
|
430
|
+
fetchAllRemotes({ cwd: repoRoot }),
|
|
431
|
+
);
|
|
432
|
+
if (!fetchResult.ok) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
let fastForwardError: Error | null = null;
|
|
437
|
+
try {
|
|
438
|
+
await pullFastForward(worktreePath);
|
|
439
|
+
printInfo(`Fast-forward pull finished for ${branch}.`);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
fastForwardError =
|
|
442
|
+
error instanceof Error ? error : new Error(String(error));
|
|
443
|
+
printWarning(
|
|
444
|
+
`Fast-forward pull failed for ${branch}. Checking for divergence before continuing...`,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const divergenceBranches = new Set<string>();
|
|
449
|
+
const sanitizeBranchName = (value: string | null | undefined) => {
|
|
450
|
+
if (!value) return null;
|
|
451
|
+
return value.replace(/^origin\//, "");
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const sanitizedBranch = sanitizeBranchName(branch);
|
|
455
|
+
if (sanitizedBranch) {
|
|
456
|
+
divergenceBranches.add(sanitizedBranch);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const sanitizedRemoteBranch = sanitizeBranchName(remoteBranch);
|
|
460
|
+
if (sanitizedRemoteBranch) {
|
|
461
|
+
divergenceBranches.add(sanitizedRemoteBranch);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const divergenceResult = await runGitStep("check branch divergence", () =>
|
|
465
|
+
getBranchDivergenceStatuses({
|
|
466
|
+
cwd: repoRoot,
|
|
467
|
+
branches: Array.from(divergenceBranches),
|
|
468
|
+
}),
|
|
469
|
+
);
|
|
470
|
+
if (!divergenceResult.ok) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const divergenceStatuses = divergenceResult.value;
|
|
474
|
+
const divergedBranches = divergenceStatuses.filter(
|
|
475
|
+
(status) => status.remoteAhead > 0 && status.localAhead > 0,
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
if (divergedBranches.length > 0) {
|
|
479
|
+
printWarning(
|
|
480
|
+
"Potential merge conflicts detected when pulling the following local branches:",
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
divergedBranches.forEach(
|
|
484
|
+
({ branch: divergedBranch, remoteAhead, localAhead }) => {
|
|
485
|
+
const highlight =
|
|
486
|
+
divergedBranch === branch ? " (selected branch)" : "";
|
|
487
|
+
console.warn(
|
|
488
|
+
chalk.yellow(
|
|
489
|
+
` • ${divergedBranch}${highlight} remote:+${remoteAhead} local:+${localAhead}`,
|
|
490
|
+
),
|
|
491
|
+
);
|
|
492
|
+
},
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
printWarning(
|
|
496
|
+
"Resolve these divergences (e.g., rebase or merge) before launching to avoid conflicts.",
|
|
497
|
+
);
|
|
498
|
+
await waitForEnter("Press Enter to continue.");
|
|
499
|
+
printWarning(
|
|
500
|
+
"Skipping AI tool launch until divergences are resolved. Returning to main menu...",
|
|
501
|
+
);
|
|
502
|
+
return;
|
|
503
|
+
} else if (fastForwardError) {
|
|
504
|
+
printWarning(
|
|
505
|
+
`Fast-forward pull could not complete (${fastForwardError.message}). Continuing without blocking.`,
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Get tool definition and shared environment overrides
|
|
510
|
+
const [toolConfig, sharedEnv] = await Promise.all([
|
|
511
|
+
getToolById(tool),
|
|
512
|
+
getSharedEnvironment(),
|
|
513
|
+
]);
|
|
514
|
+
|
|
515
|
+
if (!toolConfig) {
|
|
516
|
+
throw new Error(`Tool not found: ${tool}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Launch selected AI tool
|
|
520
|
+
// Builtin tools use their dedicated launch functions
|
|
521
|
+
// Custom tools use the generic launchCustomAITool function
|
|
522
|
+
if (tool === "claude-code") {
|
|
523
|
+
await launchClaudeCode(worktreePath, {
|
|
524
|
+
mode:
|
|
525
|
+
mode === "resume"
|
|
526
|
+
? "resume"
|
|
527
|
+
: mode === "continue"
|
|
528
|
+
? "continue"
|
|
529
|
+
: "normal",
|
|
530
|
+
skipPermissions,
|
|
531
|
+
envOverrides: sharedEnv,
|
|
532
|
+
});
|
|
533
|
+
} else if (tool === "codex-cli") {
|
|
534
|
+
await launchCodexCLI(worktreePath, {
|
|
535
|
+
mode:
|
|
536
|
+
mode === "resume"
|
|
537
|
+
? "resume"
|
|
538
|
+
: mode === "continue"
|
|
539
|
+
? "continue"
|
|
540
|
+
: "normal",
|
|
541
|
+
bypassApprovals: skipPermissions,
|
|
542
|
+
envOverrides: sharedEnv,
|
|
543
|
+
});
|
|
544
|
+
} else {
|
|
545
|
+
// Custom tool
|
|
546
|
+
printInfo(`Launching custom tool: ${toolConfig.displayName}`);
|
|
547
|
+
await launchCustomAITool(toolConfig, {
|
|
548
|
+
mode:
|
|
549
|
+
mode === "resume"
|
|
550
|
+
? "resume"
|
|
551
|
+
: mode === "continue"
|
|
552
|
+
? "continue"
|
|
553
|
+
: "normal",
|
|
554
|
+
skipPermissions,
|
|
555
|
+
cwd: worktreePath,
|
|
556
|
+
sharedEnv,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Save session with lastUsedTool
|
|
561
|
+
await saveSession({
|
|
562
|
+
lastWorktreePath: worktreePath,
|
|
563
|
+
lastBranch: branch,
|
|
564
|
+
lastUsedTool: tool,
|
|
565
|
+
timestamp: Date.now(),
|
|
566
|
+
repositoryRoot: repoRoot,
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
printInfo("Session completed successfully. Returning to main menu...");
|
|
570
|
+
return;
|
|
571
|
+
} catch (error) {
|
|
572
|
+
// Handle recoverable errors (Git, Worktree, Codex errors)
|
|
573
|
+
if (isRecoverableError(error)) {
|
|
574
|
+
const details = error instanceof Error ? error.message : String(error);
|
|
575
|
+
printError(`Error during workflow: ${details}`);
|
|
576
|
+
await waitForErrorAcknowledgement();
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
// Re-throw non-recoverable errors
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
type UIHandler = () => Promise<SelectionResult | undefined>;
|
|
585
|
+
type WorkflowHandler = (selection: SelectionResult) => Promise<void>;
|
|
586
|
+
|
|
587
|
+
function logLoopError(error: unknown, context: "ui" | "workflow"): void {
|
|
588
|
+
const label = context === "ui" ? "UI" : "workflow";
|
|
589
|
+
if (error instanceof Error) {
|
|
590
|
+
printError(`${label} error: ${error.message}`);
|
|
591
|
+
} else {
|
|
592
|
+
printError(`${label} error: ${String(error)}`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export async function runInteractiveLoop(
|
|
597
|
+
uiHandler: UIHandler = mainInkUI,
|
|
598
|
+
workflowHandler: WorkflowHandler = handleAIToolWorkflow,
|
|
599
|
+
): Promise<void> {
|
|
600
|
+
// Main loop: UI → AI Tool → back to UI
|
|
601
|
+
while (true) {
|
|
602
|
+
let selectionResult: SelectionResult | undefined;
|
|
603
|
+
|
|
604
|
+
try {
|
|
605
|
+
selectionResult = await uiHandler();
|
|
606
|
+
} catch (error) {
|
|
607
|
+
logLoopError(error, "ui");
|
|
608
|
+
await waitForErrorAcknowledgement();
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (!selectionResult) {
|
|
613
|
+
// User quit (pressed q without making selections)
|
|
614
|
+
printInfo("Goodbye!");
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
await workflowHandler(selectionResult);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
logLoopError(error, "workflow");
|
|
622
|
+
await waitForErrorAcknowledgement();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Main entry point
|
|
629
|
+
*/
|
|
630
|
+
export async function main(): Promise<void> {
|
|
631
|
+
// Parse command line arguments
|
|
632
|
+
const args = process.argv.slice(2);
|
|
633
|
+
const showVersionFlag = args.includes("-v") || args.includes("--version");
|
|
634
|
+
const showHelpFlag = args.includes("-h") || args.includes("--help");
|
|
635
|
+
const serveCommand = args.includes("serve");
|
|
636
|
+
|
|
637
|
+
// Version flag has higher priority than help
|
|
638
|
+
if (showVersionFlag) {
|
|
639
|
+
await showVersion();
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (showHelpFlag) {
|
|
644
|
+
showHelp();
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Start Web UI server if 'serve' command is provided
|
|
649
|
+
if (serveCommand) {
|
|
650
|
+
const { startWebServer } = await import("./web/server/index.js");
|
|
651
|
+
await startWebServer();
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Check if current directory is a Git repository
|
|
656
|
+
if (!(await isGitRepository())) {
|
|
657
|
+
printError(`Current directory is not a Git repository: ${process.cwd()}`);
|
|
658
|
+
printInfo(
|
|
659
|
+
"Please run this command from within a Git repository or worktree directory.",
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
// Docker環境でよくある問題: safe.directory設定
|
|
663
|
+
printInfo(
|
|
664
|
+
"\\nIf you're running in Docker, you may need to configure Git safe.directory:",
|
|
665
|
+
);
|
|
666
|
+
printInfo(" git config --global --add safe.directory '*'");
|
|
667
|
+
printInfo("\\nOr run with DEBUG=1 for more information:");
|
|
668
|
+
printInfo(" DEBUG=1 bun run start");
|
|
669
|
+
|
|
670
|
+
await waitForErrorAcknowledgement();
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
await runInteractiveLoop();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Run the application if this module is executed directly
|
|
678
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
679
|
+
main().catch(async (error) => {
|
|
680
|
+
console.error("Fatal error:", error);
|
|
681
|
+
await waitForErrorAcknowledgement();
|
|
682
|
+
process.exit(1);
|
|
683
|
+
});
|
|
684
|
+
}
|