hjworktree-cli 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/.context-snapshots/context-snapshot-20260106-110353.md +66 -0
- package/.context-snapshots/context-snapshot-20260106-110441.md +66 -0
- package/.context-snapshots/context-snapshot-20260106-220000.md +99 -0
- package/AGENTS.md +29 -0
- package/CLAUDE.md +88 -0
- package/bin/cli.js +85 -0
- package/dist/server/index.d.ts +6 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +64 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/routes/api.d.ts +3 -0
- package/dist/server/routes/api.d.ts.map +1 -0
- package/dist/server/routes/api.js +101 -0
- package/dist/server/routes/api.js.map +1 -0
- package/dist/server/services/gitService.d.ts +13 -0
- package/dist/server/services/gitService.d.ts.map +1 -0
- package/dist/server/services/gitService.js +84 -0
- package/dist/server/services/gitService.js.map +1 -0
- package/dist/server/services/worktreeService.d.ts +17 -0
- package/dist/server/services/worktreeService.d.ts.map +1 -0
- package/dist/server/services/worktreeService.js +161 -0
- package/dist/server/services/worktreeService.js.map +1 -0
- package/dist/server/socketHandlers.d.ts +4 -0
- package/dist/server/socketHandlers.d.ts.map +1 -0
- package/dist/server/socketHandlers.js +118 -0
- package/dist/server/socketHandlers.js.map +1 -0
- package/dist/shared/constants.d.ts +10 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/constants.js +31 -0
- package/dist/shared/constants.js.map +1 -0
- package/dist/shared/types/index.d.ts +67 -0
- package/dist/shared/types/index.d.ts.map +1 -0
- package/dist/shared/types/index.js +3 -0
- package/dist/shared/types/index.js.map +1 -0
- package/dist/web/assets/index-C61yAbey.css +32 -0
- package/dist/web/assets/index-WEdVUKxb.js +53 -0
- package/dist/web/assets/index-WEdVUKxb.js.map +1 -0
- package/dist/web/index.html +16 -0
- package/package.json +63 -0
- package/server/index.ts +75 -0
- package/server/routes/api.ts +108 -0
- package/server/services/gitService.ts +91 -0
- package/server/services/worktreeService.ts +181 -0
- package/server/socketHandlers.ts +157 -0
- package/shared/constants.ts +35 -0
- package/shared/types/index.ts +92 -0
- package/tsconfig.json +20 -0
- package/web/index.html +15 -0
- package/web/src/App.tsx +65 -0
- package/web/src/components/Layout/Header.tsx +29 -0
- package/web/src/components/Layout/LeftNavBar.tsx +67 -0
- package/web/src/components/Layout/MainLayout.tsx +23 -0
- package/web/src/components/Layout/StepContainer.tsx +71 -0
- package/web/src/components/Setup/AgentSelector.tsx +27 -0
- package/web/src/components/Setup/BranchSelector.tsx +28 -0
- package/web/src/components/Setup/SetupPanel.tsx +32 -0
- package/web/src/components/Setup/WorktreeCountSelector.tsx +30 -0
- package/web/src/components/Steps/AgentStep.tsx +20 -0
- package/web/src/components/Steps/BranchStep.tsx +20 -0
- package/web/src/components/Steps/WorktreeStep.tsx +41 -0
- package/web/src/components/Terminal/TerminalPanel.tsx +113 -0
- package/web/src/components/Terminal/XTerminal.tsx +203 -0
- package/web/src/hooks/useSocket.ts +80 -0
- package/web/src/main.tsx +10 -0
- package/web/src/stores/useAppStore.ts +348 -0
- package/web/src/styles/global.css +695 -0
- package/web/tsconfig.json +23 -0
- package/web/vite.config.ts +32 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import type { Branch, TerminalInfo, AgentId, AgentStatus, NavigationStep, StepStatus } from '../../../shared/types/index.js';
|
|
3
|
+
import { STEP_ORDER } from '../../../shared/types/index.js';
|
|
4
|
+
import { DEFAULT_PARALLEL_COUNT, MIN_PARALLEL_COUNT, MAX_PARALLEL_COUNT } from '../../../shared/constants.js';
|
|
5
|
+
|
|
6
|
+
interface ProjectInfo {
|
|
7
|
+
cwd: string;
|
|
8
|
+
isGitRepository: boolean;
|
|
9
|
+
currentBranch: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface AppState {
|
|
13
|
+
// Navigation
|
|
14
|
+
step: NavigationStep;
|
|
15
|
+
completedSteps: NavigationStep[];
|
|
16
|
+
|
|
17
|
+
// Project info
|
|
18
|
+
projectInfo: ProjectInfo | null;
|
|
19
|
+
|
|
20
|
+
// Branches
|
|
21
|
+
branches: Branch[];
|
|
22
|
+
selectedBranch: string | null;
|
|
23
|
+
|
|
24
|
+
// Agent selection
|
|
25
|
+
selectedAgent: AgentId;
|
|
26
|
+
|
|
27
|
+
// Worktree count
|
|
28
|
+
worktreeCount: number;
|
|
29
|
+
|
|
30
|
+
// Terminals
|
|
31
|
+
terminals: TerminalInfo[];
|
|
32
|
+
activeTerminalIndex: number;
|
|
33
|
+
|
|
34
|
+
// Loading states
|
|
35
|
+
isLoading: boolean;
|
|
36
|
+
loadingMessage: string;
|
|
37
|
+
|
|
38
|
+
// Error state
|
|
39
|
+
error: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface AppActions {
|
|
43
|
+
// Navigation
|
|
44
|
+
setStep: (step: NavigationStep) => void;
|
|
45
|
+
goToStep: (step: NavigationStep) => void;
|
|
46
|
+
goToNext: () => void;
|
|
47
|
+
goToPrev: () => void;
|
|
48
|
+
canNavigateTo: (step: NavigationStep) => boolean;
|
|
49
|
+
isStepComplete: (step: NavigationStep) => boolean;
|
|
50
|
+
getStepStatus: (step: NavigationStep) => StepStatus;
|
|
51
|
+
|
|
52
|
+
// Project info
|
|
53
|
+
setProjectInfo: (info: ProjectInfo) => void;
|
|
54
|
+
fetchProjectInfo: () => Promise<void>;
|
|
55
|
+
|
|
56
|
+
// Branches
|
|
57
|
+
setBranches: (branches: Branch[]) => void;
|
|
58
|
+
setSelectedBranch: (branch: string | null) => void;
|
|
59
|
+
fetchBranches: () => Promise<void>;
|
|
60
|
+
|
|
61
|
+
// Agent selection
|
|
62
|
+
setSelectedAgent: (agent: AgentId) => void;
|
|
63
|
+
|
|
64
|
+
// Worktree count
|
|
65
|
+
setWorktreeCount: (count: number) => void;
|
|
66
|
+
incrementWorktreeCount: () => void;
|
|
67
|
+
decrementWorktreeCount: () => void;
|
|
68
|
+
|
|
69
|
+
// Terminals
|
|
70
|
+
addTerminal: (terminal: TerminalInfo) => void;
|
|
71
|
+
updateTerminalStatus: (sessionId: string, status: AgentStatus) => void;
|
|
72
|
+
removeTerminal: (sessionId: string) => void;
|
|
73
|
+
setActiveTerminal: (index: number) => void;
|
|
74
|
+
clearAllTerminals: () => void;
|
|
75
|
+
|
|
76
|
+
// Execute
|
|
77
|
+
execute: () => Promise<void>;
|
|
78
|
+
|
|
79
|
+
// Loading
|
|
80
|
+
setLoading: (loading: boolean, message?: string) => void;
|
|
81
|
+
|
|
82
|
+
// Error
|
|
83
|
+
setError: (error: string | null) => void;
|
|
84
|
+
|
|
85
|
+
// Reset
|
|
86
|
+
reset: () => void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type AppStore = AppState & AppActions;
|
|
90
|
+
|
|
91
|
+
const initialState: AppState = {
|
|
92
|
+
step: 'branch',
|
|
93
|
+
completedSteps: [],
|
|
94
|
+
projectInfo: null,
|
|
95
|
+
branches: [],
|
|
96
|
+
selectedBranch: null,
|
|
97
|
+
selectedAgent: 'claude',
|
|
98
|
+
worktreeCount: DEFAULT_PARALLEL_COUNT,
|
|
99
|
+
terminals: [],
|
|
100
|
+
activeTerminalIndex: 0,
|
|
101
|
+
isLoading: false,
|
|
102
|
+
loadingMessage: '',
|
|
103
|
+
error: null,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const useAppStore = create<AppStore>((set, get) => ({
|
|
107
|
+
...initialState,
|
|
108
|
+
|
|
109
|
+
// Navigation
|
|
110
|
+
setStep: (step) => set({ step }),
|
|
111
|
+
|
|
112
|
+
goToStep: (targetStep) => {
|
|
113
|
+
const { canNavigateTo, step, isStepComplete, completedSteps } = get();
|
|
114
|
+
if (!canNavigateTo(targetStep)) return;
|
|
115
|
+
|
|
116
|
+
// Mark current step as complete if valid
|
|
117
|
+
const newCompleted = [...completedSteps];
|
|
118
|
+
if (isStepComplete(step) && !newCompleted.includes(step)) {
|
|
119
|
+
newCompleted.push(step);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
set({ step: targetStep, completedSteps: newCompleted });
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
goToNext: () => {
|
|
126
|
+
const { step, isStepComplete, completedSteps } = get();
|
|
127
|
+
const currentIndex = STEP_ORDER.indexOf(step);
|
|
128
|
+
|
|
129
|
+
if (currentIndex < STEP_ORDER.length - 1 && isStepComplete(step)) {
|
|
130
|
+
const newCompleted = [...completedSteps];
|
|
131
|
+
if (!newCompleted.includes(step)) {
|
|
132
|
+
newCompleted.push(step);
|
|
133
|
+
}
|
|
134
|
+
set({
|
|
135
|
+
step: STEP_ORDER[currentIndex + 1],
|
|
136
|
+
completedSteps: newCompleted
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
goToPrev: () => {
|
|
142
|
+
const { step } = get();
|
|
143
|
+
const currentIndex = STEP_ORDER.indexOf(step);
|
|
144
|
+
|
|
145
|
+
if (currentIndex > 0) {
|
|
146
|
+
set({ step: STEP_ORDER[currentIndex - 1] });
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
canNavigateTo: (targetStep) => {
|
|
151
|
+
const { step, completedSteps, isStepComplete } = get();
|
|
152
|
+
const currentIndex = STEP_ORDER.indexOf(step);
|
|
153
|
+
const targetIndex = STEP_ORDER.indexOf(targetStep);
|
|
154
|
+
|
|
155
|
+
// Can always go to current step
|
|
156
|
+
if (targetIndex === currentIndex) return true;
|
|
157
|
+
|
|
158
|
+
// Can always go back to completed or earlier steps
|
|
159
|
+
if (targetIndex < currentIndex) return true;
|
|
160
|
+
|
|
161
|
+
// Can only go forward if all previous steps are complete
|
|
162
|
+
for (let i = 0; i < targetIndex; i++) {
|
|
163
|
+
const checkStep = STEP_ORDER[i];
|
|
164
|
+
if (!completedSteps.includes(checkStep) && !isStepComplete(checkStep)) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
isStepComplete: (checkStep) => {
|
|
172
|
+
const { selectedBranch, selectedAgent, worktreeCount, terminals } = get();
|
|
173
|
+
|
|
174
|
+
switch (checkStep) {
|
|
175
|
+
case 'branch':
|
|
176
|
+
return selectedBranch !== null;
|
|
177
|
+
case 'agent':
|
|
178
|
+
return selectedAgent !== null;
|
|
179
|
+
case 'worktree':
|
|
180
|
+
return worktreeCount >= MIN_PARALLEL_COUNT;
|
|
181
|
+
case 'running':
|
|
182
|
+
return terminals.length > 0;
|
|
183
|
+
default:
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
getStepStatus: (checkStep) => {
|
|
189
|
+
const { step, completedSteps, isStepComplete } = get();
|
|
190
|
+
|
|
191
|
+
if (checkStep === step) return 'current';
|
|
192
|
+
if (completedSteps.includes(checkStep)) return 'completed';
|
|
193
|
+
if (isStepComplete(checkStep)) return 'completed';
|
|
194
|
+
return 'pending';
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// Project info
|
|
198
|
+
setProjectInfo: (info) => set({ projectInfo: info }),
|
|
199
|
+
|
|
200
|
+
fetchProjectInfo: async () => {
|
|
201
|
+
try {
|
|
202
|
+
const response = await fetch('/api/info');
|
|
203
|
+
if (!response.ok) throw new Error('Failed to fetch project info');
|
|
204
|
+
const info = await response.json();
|
|
205
|
+
set({ projectInfo: info });
|
|
206
|
+
} catch (error) {
|
|
207
|
+
set({ error: error instanceof Error ? error.message : 'Unknown error' });
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
// Branches
|
|
212
|
+
setBranches: (branches) => set({ branches }),
|
|
213
|
+
|
|
214
|
+
setSelectedBranch: (branch) => set({ selectedBranch: branch }),
|
|
215
|
+
|
|
216
|
+
fetchBranches: async () => {
|
|
217
|
+
try {
|
|
218
|
+
const response = await fetch('/api/branches');
|
|
219
|
+
if (!response.ok) throw new Error('Failed to fetch branches');
|
|
220
|
+
const branches = await response.json();
|
|
221
|
+
set({ branches });
|
|
222
|
+
|
|
223
|
+
// Auto-select current branch if none selected
|
|
224
|
+
const state = get();
|
|
225
|
+
if (!state.selectedBranch && branches.length > 0) {
|
|
226
|
+
const currentBranch = branches.find((b: Branch) => b.isCurrent);
|
|
227
|
+
set({ selectedBranch: currentBranch?.name || branches[0].name });
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
set({ error: error instanceof Error ? error.message : 'Unknown error' });
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
// Agent selection
|
|
235
|
+
setSelectedAgent: (agent) => set({ selectedAgent: agent }),
|
|
236
|
+
|
|
237
|
+
// Worktree count
|
|
238
|
+
setWorktreeCount: (count) => set({
|
|
239
|
+
worktreeCount: Math.max(MIN_PARALLEL_COUNT, Math.min(MAX_PARALLEL_COUNT, count))
|
|
240
|
+
}),
|
|
241
|
+
|
|
242
|
+
incrementWorktreeCount: () => {
|
|
243
|
+
const { worktreeCount } = get();
|
|
244
|
+
if (worktreeCount < MAX_PARALLEL_COUNT) {
|
|
245
|
+
set({ worktreeCount: worktreeCount + 1 });
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
decrementWorktreeCount: () => {
|
|
250
|
+
const { worktreeCount } = get();
|
|
251
|
+
if (worktreeCount > MIN_PARALLEL_COUNT) {
|
|
252
|
+
set({ worktreeCount: worktreeCount - 1 });
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
// Terminals
|
|
257
|
+
addTerminal: (terminal) => set((state) => ({
|
|
258
|
+
terminals: [...state.terminals, terminal]
|
|
259
|
+
})),
|
|
260
|
+
|
|
261
|
+
updateTerminalStatus: (sessionId, status) => set((state) => ({
|
|
262
|
+
terminals: state.terminals.map((t) =>
|
|
263
|
+
t.sessionId === sessionId ? { ...t, status } : t
|
|
264
|
+
)
|
|
265
|
+
})),
|
|
266
|
+
|
|
267
|
+
removeTerminal: (sessionId) => set((state) => {
|
|
268
|
+
const newTerminals = state.terminals.filter((t) => t.sessionId !== sessionId);
|
|
269
|
+
const newActiveIndex = Math.min(state.activeTerminalIndex, Math.max(0, newTerminals.length - 1));
|
|
270
|
+
return {
|
|
271
|
+
terminals: newTerminals,
|
|
272
|
+
activeTerminalIndex: newActiveIndex,
|
|
273
|
+
step: newTerminals.length === 0 ? 'branch' : state.step,
|
|
274
|
+
completedSteps: newTerminals.length === 0 ? [] : state.completedSteps,
|
|
275
|
+
};
|
|
276
|
+
}),
|
|
277
|
+
|
|
278
|
+
setActiveTerminal: (index) => set({ activeTerminalIndex: index }),
|
|
279
|
+
|
|
280
|
+
clearAllTerminals: () => set({ terminals: [], activeTerminalIndex: 0 }),
|
|
281
|
+
|
|
282
|
+
// Execute
|
|
283
|
+
execute: async () => {
|
|
284
|
+
const { selectedBranch, worktreeCount, selectedAgent, setLoading, setError, setStep, addTerminal } = get();
|
|
285
|
+
|
|
286
|
+
if (!selectedBranch) {
|
|
287
|
+
setError('Please select a branch');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
setLoading(true, 'Creating worktrees...');
|
|
292
|
+
setError(null);
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const response = await fetch('/api/worktrees', {
|
|
296
|
+
method: 'POST',
|
|
297
|
+
headers: { 'Content-Type': 'application/json' },
|
|
298
|
+
body: JSON.stringify({
|
|
299
|
+
branch: selectedBranch,
|
|
300
|
+
count: worktreeCount,
|
|
301
|
+
agentType: selectedAgent,
|
|
302
|
+
}),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
if (!response.ok) {
|
|
306
|
+
const data = await response.json();
|
|
307
|
+
throw new Error(data.error || 'Failed to create worktrees');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const { worktrees } = await response.json();
|
|
311
|
+
|
|
312
|
+
// Create terminal info for each worktree
|
|
313
|
+
for (const worktree of worktrees) {
|
|
314
|
+
const sessionId = `${worktree.name}-${Date.now()}`;
|
|
315
|
+
addTerminal({
|
|
316
|
+
sessionId,
|
|
317
|
+
worktreePath: worktree.path,
|
|
318
|
+
worktreeName: worktree.name,
|
|
319
|
+
branchName: worktree.branch,
|
|
320
|
+
agentType: selectedAgent,
|
|
321
|
+
status: 'initializing',
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Mark all setup steps as complete and transition to running
|
|
326
|
+
set({
|
|
327
|
+
step: 'running',
|
|
328
|
+
completedSteps: ['branch', 'agent', 'worktree']
|
|
329
|
+
});
|
|
330
|
+
} catch (error) {
|
|
331
|
+
setError(error instanceof Error ? error.message : 'Unknown error');
|
|
332
|
+
} finally {
|
|
333
|
+
setLoading(false);
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
// Loading
|
|
338
|
+
setLoading: (loading, message = '') => set({
|
|
339
|
+
isLoading: loading,
|
|
340
|
+
loadingMessage: message,
|
|
341
|
+
}),
|
|
342
|
+
|
|
343
|
+
// Error
|
|
344
|
+
setError: (error) => set({ error }),
|
|
345
|
+
|
|
346
|
+
// Reset
|
|
347
|
+
reset: () => set(initialState),
|
|
348
|
+
}));
|