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.
Files changed (68) hide show
  1. package/.context-snapshots/context-snapshot-20260106-110353.md +66 -0
  2. package/.context-snapshots/context-snapshot-20260106-110441.md +66 -0
  3. package/.context-snapshots/context-snapshot-20260106-220000.md +99 -0
  4. package/AGENTS.md +29 -0
  5. package/CLAUDE.md +88 -0
  6. package/bin/cli.js +85 -0
  7. package/dist/server/index.d.ts +6 -0
  8. package/dist/server/index.d.ts.map +1 -0
  9. package/dist/server/index.js +64 -0
  10. package/dist/server/index.js.map +1 -0
  11. package/dist/server/routes/api.d.ts +3 -0
  12. package/dist/server/routes/api.d.ts.map +1 -0
  13. package/dist/server/routes/api.js +101 -0
  14. package/dist/server/routes/api.js.map +1 -0
  15. package/dist/server/services/gitService.d.ts +13 -0
  16. package/dist/server/services/gitService.d.ts.map +1 -0
  17. package/dist/server/services/gitService.js +84 -0
  18. package/dist/server/services/gitService.js.map +1 -0
  19. package/dist/server/services/worktreeService.d.ts +17 -0
  20. package/dist/server/services/worktreeService.d.ts.map +1 -0
  21. package/dist/server/services/worktreeService.js +161 -0
  22. package/dist/server/services/worktreeService.js.map +1 -0
  23. package/dist/server/socketHandlers.d.ts +4 -0
  24. package/dist/server/socketHandlers.d.ts.map +1 -0
  25. package/dist/server/socketHandlers.js +118 -0
  26. package/dist/server/socketHandlers.js.map +1 -0
  27. package/dist/shared/constants.d.ts +10 -0
  28. package/dist/shared/constants.d.ts.map +1 -0
  29. package/dist/shared/constants.js +31 -0
  30. package/dist/shared/constants.js.map +1 -0
  31. package/dist/shared/types/index.d.ts +67 -0
  32. package/dist/shared/types/index.d.ts.map +1 -0
  33. package/dist/shared/types/index.js +3 -0
  34. package/dist/shared/types/index.js.map +1 -0
  35. package/dist/web/assets/index-C61yAbey.css +32 -0
  36. package/dist/web/assets/index-WEdVUKxb.js +53 -0
  37. package/dist/web/assets/index-WEdVUKxb.js.map +1 -0
  38. package/dist/web/index.html +16 -0
  39. package/package.json +63 -0
  40. package/server/index.ts +75 -0
  41. package/server/routes/api.ts +108 -0
  42. package/server/services/gitService.ts +91 -0
  43. package/server/services/worktreeService.ts +181 -0
  44. package/server/socketHandlers.ts +157 -0
  45. package/shared/constants.ts +35 -0
  46. package/shared/types/index.ts +92 -0
  47. package/tsconfig.json +20 -0
  48. package/web/index.html +15 -0
  49. package/web/src/App.tsx +65 -0
  50. package/web/src/components/Layout/Header.tsx +29 -0
  51. package/web/src/components/Layout/LeftNavBar.tsx +67 -0
  52. package/web/src/components/Layout/MainLayout.tsx +23 -0
  53. package/web/src/components/Layout/StepContainer.tsx +71 -0
  54. package/web/src/components/Setup/AgentSelector.tsx +27 -0
  55. package/web/src/components/Setup/BranchSelector.tsx +28 -0
  56. package/web/src/components/Setup/SetupPanel.tsx +32 -0
  57. package/web/src/components/Setup/WorktreeCountSelector.tsx +30 -0
  58. package/web/src/components/Steps/AgentStep.tsx +20 -0
  59. package/web/src/components/Steps/BranchStep.tsx +20 -0
  60. package/web/src/components/Steps/WorktreeStep.tsx +41 -0
  61. package/web/src/components/Terminal/TerminalPanel.tsx +113 -0
  62. package/web/src/components/Terminal/XTerminal.tsx +203 -0
  63. package/web/src/hooks/useSocket.ts +80 -0
  64. package/web/src/main.tsx +10 -0
  65. package/web/src/stores/useAppStore.ts +348 -0
  66. package/web/src/styles/global.css +695 -0
  67. package/web/tsconfig.json +23 -0
  68. 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
+ }));