@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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BranchInfo,
|
|
3
|
+
BranchItem,
|
|
4
|
+
BranchType,
|
|
5
|
+
WorktreeStatus,
|
|
6
|
+
WorktreeInfo,
|
|
7
|
+
} from "../types.js";
|
|
8
|
+
import stringWidth from "string-width";
|
|
9
|
+
|
|
10
|
+
// Icon mappings
|
|
11
|
+
const branchIcons: Record<BranchType, string> = {
|
|
12
|
+
main: "⚡",
|
|
13
|
+
develop: "⚡",
|
|
14
|
+
feature: "✨",
|
|
15
|
+
hotfix: "🔥",
|
|
16
|
+
release: "🚀",
|
|
17
|
+
other: "📌",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const worktreeIcons: Record<Exclude<WorktreeStatus, undefined>, string> = {
|
|
21
|
+
active: "🟢",
|
|
22
|
+
inaccessible: "🟠",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const changeIcons = {
|
|
26
|
+
current: "⭐",
|
|
27
|
+
hasChanges: "✏️",
|
|
28
|
+
unpushed: "⬆️",
|
|
29
|
+
openPR: "🔀",
|
|
30
|
+
mergedPR: "✅",
|
|
31
|
+
warning: "⚠️",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const remoteIcon = "☁";
|
|
35
|
+
|
|
36
|
+
// Some icons are treated as double-width by string-width even though they render
|
|
37
|
+
// as a single column in many terminals (e.g. ☁). Provide explicit overrides to
|
|
38
|
+
// keep the column layout consistent across environments.
|
|
39
|
+
const iconWidthOverrides: Record<string, number> = {
|
|
40
|
+
[remoteIcon]: 1,
|
|
41
|
+
"☁️": 1,
|
|
42
|
+
"☁︎": 1,
|
|
43
|
+
"⬆️": 1,
|
|
44
|
+
"⬆︎": 1,
|
|
45
|
+
"⬆": 1,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export interface FormatOptions {
|
|
49
|
+
hasChanges?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Converts BranchInfo to BranchItem with display properties
|
|
54
|
+
*/
|
|
55
|
+
export function formatBranchItem(
|
|
56
|
+
branch: BranchInfo,
|
|
57
|
+
options: FormatOptions = {},
|
|
58
|
+
): BranchItem {
|
|
59
|
+
const hasChanges = options.hasChanges ?? false;
|
|
60
|
+
const COLUMN_WIDTH = 2; // Fixed width for each icon column
|
|
61
|
+
|
|
62
|
+
// Helper to pad icon to fixed width
|
|
63
|
+
const padIcon = (icon: string): string => {
|
|
64
|
+
const width = iconWidthOverrides[icon] ?? stringWidth(icon);
|
|
65
|
+
const padding = Math.max(0, COLUMN_WIDTH - width);
|
|
66
|
+
return icon + " ".repeat(padding);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Column 1: Branch type icon (always present)
|
|
70
|
+
const branchTypeIcon = padIcon(branchIcons[branch.branchType]);
|
|
71
|
+
|
|
72
|
+
// Column 2: Worktree status icon
|
|
73
|
+
let worktreeStatus: WorktreeStatus;
|
|
74
|
+
let worktreeIcon: string;
|
|
75
|
+
if (branch.worktree) {
|
|
76
|
+
if (branch.worktree.isAccessible === false) {
|
|
77
|
+
worktreeStatus = "inaccessible";
|
|
78
|
+
worktreeIcon = padIcon(worktreeIcons.inaccessible);
|
|
79
|
+
} else {
|
|
80
|
+
worktreeStatus = "active";
|
|
81
|
+
worktreeIcon = padIcon(worktreeIcons.active);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
worktreeIcon = " ".repeat(COLUMN_WIDTH);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Column 3: Change status icon (priority: ✏️ > ⬆️ > 🔀 > ✅ > ⚠️ > ⭐)
|
|
88
|
+
let changesIcon: string;
|
|
89
|
+
if (hasChanges) {
|
|
90
|
+
changesIcon = padIcon(changeIcons.hasChanges);
|
|
91
|
+
} else if (branch.hasUnpushedCommits) {
|
|
92
|
+
changesIcon = padIcon(changeIcons.unpushed);
|
|
93
|
+
} else if (branch.openPR) {
|
|
94
|
+
changesIcon = padIcon(changeIcons.openPR);
|
|
95
|
+
} else if (branch.mergedPR) {
|
|
96
|
+
changesIcon = padIcon(changeIcons.mergedPR);
|
|
97
|
+
} else if (branch.worktree?.isAccessible === false) {
|
|
98
|
+
changesIcon = padIcon(changeIcons.warning);
|
|
99
|
+
} else if (branch.isCurrent) {
|
|
100
|
+
changesIcon = padIcon(changeIcons.current);
|
|
101
|
+
} else {
|
|
102
|
+
changesIcon = " ".repeat(COLUMN_WIDTH);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Column 4: Remote icon
|
|
106
|
+
let remoteIconStr: string;
|
|
107
|
+
if (branch.type === "remote") {
|
|
108
|
+
remoteIconStr = padIcon(remoteIcon);
|
|
109
|
+
} else {
|
|
110
|
+
remoteIconStr = " ".repeat(COLUMN_WIDTH);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Build label with fixed-width columns
|
|
114
|
+
// Format: [Type][Worktree][Changes][Remote] BranchName
|
|
115
|
+
const label = `${branchTypeIcon}${worktreeIcon}${changesIcon}${remoteIconStr}${branch.name}`;
|
|
116
|
+
|
|
117
|
+
// Collect icons for compatibility
|
|
118
|
+
const icons: string[] = [];
|
|
119
|
+
icons.push(branchIcons[branch.branchType]);
|
|
120
|
+
if (worktreeStatus) {
|
|
121
|
+
icons.push(
|
|
122
|
+
worktreeStatus === "active"
|
|
123
|
+
? worktreeIcons.active
|
|
124
|
+
: worktreeIcons.inaccessible,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
// Add change icon based on priority
|
|
128
|
+
if (hasChanges) {
|
|
129
|
+
icons.push(changeIcons.hasChanges);
|
|
130
|
+
} else if (branch.hasUnpushedCommits) {
|
|
131
|
+
icons.push(changeIcons.unpushed);
|
|
132
|
+
} else if (branch.openPR) {
|
|
133
|
+
icons.push(changeIcons.openPR);
|
|
134
|
+
} else if (branch.mergedPR) {
|
|
135
|
+
icons.push(changeIcons.mergedPR);
|
|
136
|
+
} else if (branch.worktree?.isAccessible === false) {
|
|
137
|
+
icons.push(changeIcons.warning);
|
|
138
|
+
} else if (branch.isCurrent) {
|
|
139
|
+
icons.push(changeIcons.current);
|
|
140
|
+
}
|
|
141
|
+
if (branch.type === "remote") {
|
|
142
|
+
icons.push(remoteIcon);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
// Copy all properties from BranchInfo
|
|
147
|
+
...branch,
|
|
148
|
+
// Add display properties
|
|
149
|
+
icons,
|
|
150
|
+
worktreeStatus,
|
|
151
|
+
hasChanges,
|
|
152
|
+
label,
|
|
153
|
+
value: branch.name,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Sorts branches according to the priority rules:
|
|
159
|
+
* 1. Current branch (highest priority)
|
|
160
|
+
* 2. main branch
|
|
161
|
+
* 3. develop branch (only if main exists)
|
|
162
|
+
* 4. Branches with worktree
|
|
163
|
+
* 5. Latest commit timestamp (descending) within same worktree status
|
|
164
|
+
* 6. Local branches
|
|
165
|
+
* 7. Alphabetical order by name
|
|
166
|
+
*/
|
|
167
|
+
function sortBranches(
|
|
168
|
+
branches: BranchInfo[],
|
|
169
|
+
worktreeMap: Map<string, WorktreeInfo>,
|
|
170
|
+
): BranchInfo[] {
|
|
171
|
+
// Check if main branch exists
|
|
172
|
+
const hasMainBranch = branches.some((b) => b.branchType === "main");
|
|
173
|
+
|
|
174
|
+
return [...branches].sort((a, b) => {
|
|
175
|
+
// 1. Current branch is highest priority
|
|
176
|
+
if (a.isCurrent && !b.isCurrent) return -1;
|
|
177
|
+
if (!a.isCurrent && b.isCurrent) return 1;
|
|
178
|
+
|
|
179
|
+
// 2. main branch is second priority
|
|
180
|
+
if (a.branchType === "main" && b.branchType !== "main") return -1;
|
|
181
|
+
if (a.branchType !== "main" && b.branchType === "main") return 1;
|
|
182
|
+
|
|
183
|
+
// 3. develop branch is third priority (only if main exists)
|
|
184
|
+
if (hasMainBranch) {
|
|
185
|
+
if (a.branchType === "develop" && b.branchType !== "develop") return -1;
|
|
186
|
+
if (a.branchType !== "develop" && b.branchType === "develop") return 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 4. Branches with worktree are prioritized
|
|
190
|
+
const aHasWorktree = worktreeMap.has(a.name) || !!a.worktree;
|
|
191
|
+
const bHasWorktree = worktreeMap.has(b.name) || !!b.worktree;
|
|
192
|
+
if (aHasWorktree && !bHasWorktree) return -1;
|
|
193
|
+
if (!aHasWorktree && bHasWorktree) return 1;
|
|
194
|
+
|
|
195
|
+
// 5. Prioritize most recent commit within same worktree status
|
|
196
|
+
const aCommit = a.latestCommitTimestamp ?? 0;
|
|
197
|
+
const bCommit = b.latestCommitTimestamp ?? 0;
|
|
198
|
+
if (aCommit !== bCommit) {
|
|
199
|
+
return bCommit - aCommit;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 6. Local branches are prioritized over remote-only
|
|
203
|
+
const aIsLocal = a.type === "local";
|
|
204
|
+
const bIsLocal = b.type === "local";
|
|
205
|
+
if (aIsLocal && !bIsLocal) return -1;
|
|
206
|
+
if (!aIsLocal && bIsLocal) return 1;
|
|
207
|
+
|
|
208
|
+
// 7. Alphabetical order by name
|
|
209
|
+
return a.name.localeCompare(b.name);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Converts an array of BranchInfo to BranchItem array with sorting
|
|
215
|
+
*/
|
|
216
|
+
export function formatBranchItems(
|
|
217
|
+
branches: BranchInfo[],
|
|
218
|
+
worktreeMap: Map<string, WorktreeInfo> = new Map(),
|
|
219
|
+
): BranchItem[] {
|
|
220
|
+
const sortedBranches = sortBranches(branches, worktreeMap);
|
|
221
|
+
return sortedBranches.map((branch) => formatBranchItem(branch));
|
|
222
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { BranchInfo, Statistics } from "../types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calculates statistics from branch data
|
|
5
|
+
* @param branches Array of BranchInfo
|
|
6
|
+
* @param changedBranches Optional set of branch names with uncommitted changes
|
|
7
|
+
* @returns Statistics object
|
|
8
|
+
*/
|
|
9
|
+
export function calculateStatistics(
|
|
10
|
+
branches: BranchInfo[],
|
|
11
|
+
changedBranches: Set<string> = new Set(),
|
|
12
|
+
): Statistics {
|
|
13
|
+
let localCount = 0;
|
|
14
|
+
let remoteCount = 0;
|
|
15
|
+
let worktreeCount = 0;
|
|
16
|
+
let changesCount = 0;
|
|
17
|
+
|
|
18
|
+
for (const branch of branches) {
|
|
19
|
+
// Count by type
|
|
20
|
+
if (branch.type === "local") {
|
|
21
|
+
localCount++;
|
|
22
|
+
|
|
23
|
+
// Count worktrees (only for local branches)
|
|
24
|
+
if (branch.worktree) {
|
|
25
|
+
worktreeCount++;
|
|
26
|
+
|
|
27
|
+
// Count changes (only for branches with worktrees)
|
|
28
|
+
if (changedBranches.has(branch.name)) {
|
|
29
|
+
changesCount++;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} else if (branch.type === "remote") {
|
|
33
|
+
remoteCount++;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
localCount,
|
|
39
|
+
remoteCount,
|
|
40
|
+
worktreeCount,
|
|
41
|
+
changesCount,
|
|
42
|
+
lastUpdated: new Date(),
|
|
43
|
+
};
|
|
44
|
+
}
|
package/src/codex.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { platform } from "os";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { createChildStdio, getTerminalStreams } from "./utils/terminal.js";
|
|
6
|
+
|
|
7
|
+
const CODEX_CLI_PACKAGE = "@openai/codex@latest";
|
|
8
|
+
const DEFAULT_CODEX_ARGS = [
|
|
9
|
+
"--enable",
|
|
10
|
+
"web_search_request",
|
|
11
|
+
"--model=gpt-5.1-codex",
|
|
12
|
+
"--sandbox",
|
|
13
|
+
"workspace-write",
|
|
14
|
+
"-c",
|
|
15
|
+
"model_reasoning_effort=high",
|
|
16
|
+
"-c",
|
|
17
|
+
"model_reasoning_summaries=detailed",
|
|
18
|
+
"-c",
|
|
19
|
+
"sandbox_workspace_write.network_access=true",
|
|
20
|
+
"-c",
|
|
21
|
+
"shell_environment_policy.inherit=all",
|
|
22
|
+
"-c",
|
|
23
|
+
"shell_environment_policy.ignore_default_excludes=true",
|
|
24
|
+
"-c",
|
|
25
|
+
"shell_environment_policy.experimental_use_profile=true",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export class CodexError extends Error {
|
|
29
|
+
constructor(
|
|
30
|
+
message: string,
|
|
31
|
+
public cause?: unknown,
|
|
32
|
+
) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "CodexError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function launchCodexCLI(
|
|
39
|
+
worktreePath: string,
|
|
40
|
+
options: {
|
|
41
|
+
mode?: "normal" | "continue" | "resume";
|
|
42
|
+
extraArgs?: string[];
|
|
43
|
+
bypassApprovals?: boolean;
|
|
44
|
+
envOverrides?: Record<string, string>;
|
|
45
|
+
} = {},
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
const terminal = getTerminalStreams();
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
if (!existsSync(worktreePath)) {
|
|
51
|
+
throw new Error(`Worktree path does not exist: ${worktreePath}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(chalk.blue("🚀 Launching Codex CLI..."));
|
|
55
|
+
console.log(chalk.gray(` Working directory: ${worktreePath}`));
|
|
56
|
+
|
|
57
|
+
const args: string[] = [];
|
|
58
|
+
|
|
59
|
+
switch (options.mode) {
|
|
60
|
+
case "continue":
|
|
61
|
+
args.push("resume", "--last");
|
|
62
|
+
console.log(chalk.cyan(" ⏭️ Resuming last Codex session"));
|
|
63
|
+
break;
|
|
64
|
+
case "resume":
|
|
65
|
+
args.push("resume");
|
|
66
|
+
console.log(chalk.cyan(" 🔄 Resume command"));
|
|
67
|
+
break;
|
|
68
|
+
case "normal":
|
|
69
|
+
default:
|
|
70
|
+
console.log(chalk.green(" ✨ Starting new session"));
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (options.bypassApprovals) {
|
|
75
|
+
args.push("--yolo");
|
|
76
|
+
console.log(chalk.yellow(" ⚠️ Bypassing approvals and sandbox"));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (options.extraArgs && options.extraArgs.length > 0) {
|
|
80
|
+
args.push(...options.extraArgs);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
args.push(...DEFAULT_CODEX_ARGS);
|
|
84
|
+
|
|
85
|
+
terminal.exitRawMode();
|
|
86
|
+
|
|
87
|
+
const childStdio = createChildStdio();
|
|
88
|
+
|
|
89
|
+
const env = { ...process.env, ...(options.envOverrides ?? {}) };
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await execa("bunx", [CODEX_CLI_PACKAGE, ...args], {
|
|
93
|
+
cwd: worktreePath,
|
|
94
|
+
stdin: childStdio.stdin,
|
|
95
|
+
stdout: childStdio.stdout,
|
|
96
|
+
stderr: childStdio.stderr,
|
|
97
|
+
env,
|
|
98
|
+
} as any);
|
|
99
|
+
} finally {
|
|
100
|
+
childStdio.cleanup();
|
|
101
|
+
}
|
|
102
|
+
} catch (error: any) {
|
|
103
|
+
const errorMessage =
|
|
104
|
+
error.code === "ENOENT"
|
|
105
|
+
? "bunx command not found. Please ensure Bun is installed so Codex CLI can run via bunx."
|
|
106
|
+
: `Failed to launch Codex CLI: ${error.message || "Unknown error"}`;
|
|
107
|
+
|
|
108
|
+
if (platform() === "win32") {
|
|
109
|
+
console.error(chalk.red("\n💡 Windows troubleshooting tips:"));
|
|
110
|
+
console.error(
|
|
111
|
+
chalk.yellow(
|
|
112
|
+
" 1. Confirm that Bun is installed and bunx is available",
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
console.error(
|
|
116
|
+
chalk.yellow(
|
|
117
|
+
' 2. Run "bunx @openai/codex@latest -- --help" to verify the setup',
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
console.error(
|
|
121
|
+
chalk.yellow(" 3. Restart your terminal or IDE to refresh PATH"),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
throw new CodexError(errorMessage, error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function isCodexAvailable(): Promise<boolean> {
|
|
130
|
+
try {
|
|
131
|
+
await execa("bunx", [CODEX_CLI_PACKAGE, "--help"]);
|
|
132
|
+
return true;
|
|
133
|
+
} catch (error: any) {
|
|
134
|
+
if (error.code === "ENOENT") {
|
|
135
|
+
console.error(chalk.yellow("\n⚠️ bunx command not found"));
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ビルトインAIツール定義
|
|
3
|
+
*
|
|
4
|
+
* Claude Code と Codex CLI の CustomAITool 形式定義
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CustomAITool } from "../types/tools.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Claude Code のビルトイン定義
|
|
11
|
+
*/
|
|
12
|
+
export const CLAUDE_CODE_TOOL: CustomAITool = {
|
|
13
|
+
id: "claude-code",
|
|
14
|
+
displayName: "Claude Code",
|
|
15
|
+
type: "bunx",
|
|
16
|
+
command: "@anthropic-ai/claude-code@latest",
|
|
17
|
+
modeArgs: {
|
|
18
|
+
normal: [],
|
|
19
|
+
continue: ["-c"],
|
|
20
|
+
resume: ["-r"],
|
|
21
|
+
},
|
|
22
|
+
permissionSkipArgs: ["--yes"],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Codex CLI のビルトイン定義
|
|
27
|
+
*/
|
|
28
|
+
export const CODEX_CLI_TOOL: CustomAITool = {
|
|
29
|
+
id: "codex-cli",
|
|
30
|
+
displayName: "Codex CLI",
|
|
31
|
+
type: "bunx",
|
|
32
|
+
command: "@openai/codex@latest",
|
|
33
|
+
defaultArgs: ["--auto-approve", "--verbose"],
|
|
34
|
+
modeArgs: {
|
|
35
|
+
normal: [],
|
|
36
|
+
continue: ["resume", "--last"],
|
|
37
|
+
resume: ["resume"],
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* すべてのビルトインツール
|
|
43
|
+
*/
|
|
44
|
+
export const BUILTIN_TOOLS: CustomAITool[] = [CLAUDE_CODE_TOOL, CODEX_CLI_TOOL];
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* アプリケーション全体で使用する定数
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ブランチタイプ
|
|
6
|
+
export const BRANCH_TYPES = {
|
|
7
|
+
FEATURE: "feature",
|
|
8
|
+
HOTFIX: "hotfix",
|
|
9
|
+
RELEASE: "release",
|
|
10
|
+
MAIN: "main",
|
|
11
|
+
DEVELOP: "develop",
|
|
12
|
+
OTHER: "other",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
// ブランチプレフィックス
|
|
16
|
+
export const BRANCH_PREFIXES = {
|
|
17
|
+
FEATURE: "feature/",
|
|
18
|
+
HOTFIX: "hotfix/",
|
|
19
|
+
RELEASE: "release/",
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
// メインブランチ名
|
|
23
|
+
export const MAIN_BRANCHES = ["main", "master"] as const;
|
|
24
|
+
export const DEVELOP_BRANCHES = ["develop", "dev"] as const;
|
|
25
|
+
|
|
26
|
+
// 表示設定
|
|
27
|
+
export const DISPLAY_CONFIG = {
|
|
28
|
+
MAX_BRANCH_NAME_LENGTH: 50,
|
|
29
|
+
TABLE_PADDING: 2,
|
|
30
|
+
CHANGES_COLUMN_WIDTH: 10,
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
// プロンプト設定
|
|
34
|
+
export const PROMPT_CONFIG = {
|
|
35
|
+
PAGE_SIZE: 15,
|
|
36
|
+
SEARCH_ENABLED: true,
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
// Git設定
|
|
40
|
+
export const GIT_CONFIG = {
|
|
41
|
+
DEFAULT_BASE_BRANCH: "main",
|
|
42
|
+
FETCH_TIMEOUT: 30000, // 30秒
|
|
43
|
+
PUSH_RETRY_COUNT: 3,
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
// GitHub設定
|
|
47
|
+
export const GITHUB_CONFIG = {
|
|
48
|
+
PR_FETCH_LIMIT: 100,
|
|
49
|
+
DEBUG_ENV_VAR: "DEBUG_CLEANUP",
|
|
50
|
+
} as const;
|
|
51
|
+
|
|
52
|
+
// メッセージキー(国際化対応の基盤)
|
|
53
|
+
export const MESSAGE_KEYS = {
|
|
54
|
+
// エラーメッセージ
|
|
55
|
+
ERROR: {
|
|
56
|
+
NOT_GIT_REPO: "error.not_git_repo",
|
|
57
|
+
GIT_COMMAND_FAILED: "error.git_command_failed",
|
|
58
|
+
WORKTREE_CREATE_FAILED: "error.worktree_create_failed",
|
|
59
|
+
GITHUB_CLI_NOT_AVAILABLE: "error.github_cli_not_available",
|
|
60
|
+
GITHUB_AUTH_REQUIRED: "error.github_auth_required",
|
|
61
|
+
},
|
|
62
|
+
// 成功メッセージ
|
|
63
|
+
SUCCESS: {
|
|
64
|
+
WORKTREE_CREATED: "success.worktree_created",
|
|
65
|
+
BRANCH_CREATED: "success.branch_created",
|
|
66
|
+
CHANGES_COMMITTED: "success.changes_committed",
|
|
67
|
+
CHANGES_PUSHED: "success.changes_pushed",
|
|
68
|
+
CLEANUP_COMPLETED: "success.cleanup_completed",
|
|
69
|
+
},
|
|
70
|
+
// 情報メッセージ
|
|
71
|
+
INFO: {
|
|
72
|
+
LOADING: "info.loading",
|
|
73
|
+
PROCESSING: "info.processing",
|
|
74
|
+
FETCHING_DATA: "info.fetching_data",
|
|
75
|
+
},
|
|
76
|
+
} as const;
|
|
77
|
+
|
|
78
|
+
// English messages (default)
|
|
79
|
+
export const MESSAGES_EN = {
|
|
80
|
+
[MESSAGE_KEYS.ERROR.NOT_GIT_REPO]: "This directory is not a Git repository",
|
|
81
|
+
[MESSAGE_KEYS.ERROR.GIT_COMMAND_FAILED]: "Failed to run the Git command",
|
|
82
|
+
[MESSAGE_KEYS.ERROR.WORKTREE_CREATE_FAILED]: "Failed to create the worktree",
|
|
83
|
+
[MESSAGE_KEYS.ERROR.GITHUB_CLI_NOT_AVAILABLE]: "GitHub CLI is not installed",
|
|
84
|
+
[MESSAGE_KEYS.ERROR.GITHUB_AUTH_REQUIRED]:
|
|
85
|
+
"GitHub authentication is required. Please run 'gh auth login'",
|
|
86
|
+
[MESSAGE_KEYS.SUCCESS.WORKTREE_CREATED]: "Worktree created",
|
|
87
|
+
[MESSAGE_KEYS.SUCCESS.BRANCH_CREATED]: "Branch created",
|
|
88
|
+
[MESSAGE_KEYS.SUCCESS.CHANGES_COMMITTED]: "Changes committed",
|
|
89
|
+
[MESSAGE_KEYS.SUCCESS.CHANGES_PUSHED]: "Changes pushed",
|
|
90
|
+
[MESSAGE_KEYS.SUCCESS.CLEANUP_COMPLETED]: "Cleanup completed",
|
|
91
|
+
[MESSAGE_KEYS.INFO.LOADING]: "Loading...",
|
|
92
|
+
[MESSAGE_KEYS.INFO.PROCESSING]: "Processing...",
|
|
93
|
+
[MESSAGE_KEYS.INFO.FETCHING_DATA]: "Fetching data...",
|
|
94
|
+
} as const;
|
|
95
|
+
|
|
96
|
+
// Message lookup helper (multi-language ready)
|
|
97
|
+
export function getMessage(key: string): string {
|
|
98
|
+
// Currently only English is provided
|
|
99
|
+
return MESSAGES_EN[key as keyof typeof MESSAGES_EN] || key;
|
|
100
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { EnvironmentHistoryEntry } from "../types/api.js";
|
|
4
|
+
import { CONFIG_DIR } from "./tools.js";
|
|
5
|
+
|
|
6
|
+
const HISTORY_PATH = path.join(CONFIG_DIR, "env-history.json");
|
|
7
|
+
const HISTORY_LIMIT = 200;
|
|
8
|
+
|
|
9
|
+
interface HistoryFile {
|
|
10
|
+
entries: EnvironmentHistoryEntry[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function loadEnvHistory(): Promise<EnvironmentHistoryEntry[]> {
|
|
14
|
+
try {
|
|
15
|
+
const raw = await readFile(HISTORY_PATH, "utf8");
|
|
16
|
+
const parsed = JSON.parse(raw) as HistoryFile;
|
|
17
|
+
return parsed.entries ?? [];
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function recordEnvHistory(
|
|
27
|
+
entries: EnvironmentHistoryEntry[],
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
if (!entries.length) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const existing = await loadEnvHistory();
|
|
34
|
+
const updated = [...existing, ...entries];
|
|
35
|
+
const trimmed =
|
|
36
|
+
updated.length > HISTORY_LIMIT
|
|
37
|
+
? updated.slice(updated.length - HISTORY_LIMIT)
|
|
38
|
+
: updated;
|
|
39
|
+
|
|
40
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
41
|
+
const payload: HistoryFile = { entries: trimmed };
|
|
42
|
+
await writeFile(HISTORY_PATH, JSON.stringify(payload, null, 2), {
|
|
43
|
+
mode: 0o600,
|
|
44
|
+
});
|
|
45
|
+
}
|