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,113 @@
1
+ import React from 'react';
2
+ import { useAppStore } from '../../stores/useAppStore.js';
3
+ import { useSocket } from '../../hooks/useSocket.js';
4
+ import XTerminal from './XTerminal.js';
5
+
6
+ function TerminalPanel() {
7
+ const { terminals, activeTerminalIndex, setActiveTerminal, removeTerminal, clearAllTerminals, reset } = useAppStore();
8
+ const { emit } = useSocket();
9
+
10
+ const handleCloseTerminal = async (sessionId: string, e: React.MouseEvent) => {
11
+ e.stopPropagation();
12
+
13
+ // Kill the terminal via socket
14
+ emit('terminal:kill', { sessionId });
15
+
16
+ // Find the worktree name to delete
17
+ const terminal = terminals.find(t => t.sessionId === sessionId);
18
+ if (terminal) {
19
+ try {
20
+ await fetch(`/api/worktrees/${terminal.worktreeName}`, {
21
+ method: 'DELETE',
22
+ });
23
+ } catch (error) {
24
+ console.error('Failed to delete worktree:', error);
25
+ }
26
+ }
27
+
28
+ removeTerminal(sessionId);
29
+ };
30
+
31
+ const handleCloseAll = async () => {
32
+ // Kill all terminals via socket
33
+ for (const terminal of terminals) {
34
+ emit('terminal:kill', { sessionId: terminal.sessionId });
35
+ }
36
+
37
+ // Delete all worktrees
38
+ try {
39
+ await fetch('/api/worktrees', {
40
+ method: 'DELETE',
41
+ });
42
+ } catch (error) {
43
+ console.error('Failed to delete worktrees:', error);
44
+ }
45
+
46
+ clearAllTerminals();
47
+ reset();
48
+ };
49
+
50
+ const handleBackToSetup = () => {
51
+ if (terminals.length > 0) {
52
+ const confirmed = window.confirm(
53
+ 'This will terminate all running agents and delete all worktrees. Are you sure you want to go back to setup?'
54
+ );
55
+ if (confirmed) {
56
+ handleCloseAll();
57
+ }
58
+ } else {
59
+ reset();
60
+ }
61
+ };
62
+
63
+ return (
64
+ <div className="terminal-panel">
65
+ <div className="terminal-tabs">
66
+ {terminals.map((terminal, index) => (
67
+ <button
68
+ key={terminal.sessionId}
69
+ className={`terminal-tab ${index === activeTerminalIndex ? 'active' : ''}`}
70
+ onClick={() => setActiveTerminal(index)}
71
+ >
72
+ <span className={`status ${terminal.status}`} />
73
+ <span>{terminal.worktreeName}</span>
74
+ <button
75
+ className="close-btn"
76
+ onClick={(e) => handleCloseTerminal(terminal.sessionId, e)}
77
+ title="Close terminal"
78
+ >
79
+ &times;
80
+ </button>
81
+ </button>
82
+ ))}
83
+
84
+ <div className="terminal-actions">
85
+ <button onClick={handleBackToSetup}>
86
+ Back to Setup
87
+ </button>
88
+ <button className="danger" onClick={handleCloseAll}>
89
+ Close All
90
+ </button>
91
+ </div>
92
+ </div>
93
+
94
+ <div className="terminal-container">
95
+ {terminals.map((terminal, index) => (
96
+ <div
97
+ key={terminal.sessionId}
98
+ className={`terminal-wrapper ${index !== activeTerminalIndex ? 'hidden' : ''}`}
99
+ >
100
+ <XTerminal
101
+ sessionId={terminal.sessionId}
102
+ worktreePath={terminal.worktreePath}
103
+ agentType={terminal.agentType}
104
+ isActive={index === activeTerminalIndex}
105
+ />
106
+ </div>
107
+ ))}
108
+ </div>
109
+ </div>
110
+ );
111
+ }
112
+
113
+ export default TerminalPanel;
@@ -0,0 +1,203 @@
1
+ import React, { useEffect, useRef, useCallback } from 'react';
2
+ import { Terminal } from '@xterm/xterm';
3
+ import { FitAddon } from '@xterm/addon-fit';
4
+ import { useSocket } from '../../hooks/useSocket';
5
+ import { useAppStore } from '../../stores/useAppStore';
6
+ import type { AgentId, TerminalOutputData } from '../../../../shared/types/index';
7
+ import '@xterm/xterm/css/xterm.css';
8
+
9
+ interface XTerminalProps {
10
+ sessionId: string;
11
+ worktreePath: string;
12
+ agentType: AgentId;
13
+ isActive: boolean;
14
+ }
15
+
16
+ function XTerminal({ sessionId, worktreePath, agentType, isActive }: XTerminalProps) {
17
+ const terminalRef = useRef<HTMLDivElement>(null);
18
+ const termRef = useRef<Terminal | null>(null);
19
+ const fitAddonRef = useRef<FitAddon | null>(null);
20
+ const initializedRef = useRef(false);
21
+ const { emit, on, off, connected } = useSocket();
22
+ const updateTerminalStatus = useAppStore((state) => state.updateTerminalStatus);
23
+
24
+ // Initialize terminal
25
+ useEffect(() => {
26
+ if (!terminalRef.current || initializedRef.current) return;
27
+
28
+ const term = new Terminal({
29
+ cursorBlink: true,
30
+ fontSize: 14,
31
+ fontFamily: "'JetBrains Mono', Menlo, Monaco, 'Courier New', monospace",
32
+ theme: {
33
+ background: '#0d1117',
34
+ foreground: '#c9d1d9',
35
+ cursor: '#58a6ff',
36
+ cursorAccent: '#0d1117',
37
+ black: '#0d1117',
38
+ red: '#f85149',
39
+ green: '#3fb950',
40
+ yellow: '#d29922',
41
+ blue: '#58a6ff',
42
+ magenta: '#a371f7',
43
+ cyan: '#39c5cf',
44
+ white: '#c9d1d9',
45
+ brightBlack: '#6e7681',
46
+ brightRed: '#ff7b72',
47
+ brightGreen: '#56d364',
48
+ brightYellow: '#e3b341',
49
+ brightBlue: '#79c0ff',
50
+ brightMagenta: '#bc8cff',
51
+ brightCyan: '#56d4dd',
52
+ brightWhite: '#f0f6fc',
53
+ },
54
+ allowProposedApi: true,
55
+ });
56
+
57
+ const fitAddon = new FitAddon();
58
+ term.loadAddon(fitAddon);
59
+ term.open(terminalRef.current);
60
+
61
+ termRef.current = term;
62
+ fitAddonRef.current = fitAddon;
63
+ initializedRef.current = true;
64
+
65
+ // Initial fit
66
+ setTimeout(() => {
67
+ fitAddon.fit();
68
+ }, 0);
69
+
70
+ return () => {
71
+ term.dispose();
72
+ initializedRef.current = false;
73
+ };
74
+ }, []);
75
+
76
+ // Handle socket connection and terminal creation
77
+ useEffect(() => {
78
+ if (!termRef.current || !connected) return;
79
+
80
+ const term = termRef.current;
81
+ const fitAddon = fitAddonRef.current;
82
+
83
+ // Handle terminal output
84
+ const handleOutput = (data: unknown) => {
85
+ const outputData = data as TerminalOutputData;
86
+ if (outputData.sessionId === sessionId) {
87
+ term.write(outputData.data);
88
+ }
89
+ };
90
+
91
+ // Handle terminal created
92
+ const handleCreated = (data: unknown) => {
93
+ const { sessionId: createdId } = data as { sessionId: string };
94
+ if (createdId === sessionId) {
95
+ updateTerminalStatus(sessionId, 'running');
96
+ }
97
+ };
98
+
99
+ // Handle terminal exit
100
+ const handleExit = (data: unknown) => {
101
+ const { sessionId: exitedId } = data as { sessionId: string };
102
+ if (exitedId === sessionId) {
103
+ updateTerminalStatus(sessionId, 'stopped');
104
+ term.write('\r\n[Process exited]\r\n');
105
+ }
106
+ };
107
+
108
+ // Handle terminal error
109
+ const handleError = (data: unknown) => {
110
+ const { sessionId: errorId, error } = data as { sessionId: string; error: string };
111
+ if (errorId === sessionId) {
112
+ updateTerminalStatus(sessionId, 'error');
113
+ term.write(`\r\n[Error: ${error}]\r\n`);
114
+ }
115
+ };
116
+
117
+ // Register event handlers
118
+ on('terminal:output', handleOutput);
119
+ on('terminal:created', handleCreated);
120
+ on('terminal:exit', handleExit);
121
+ on('terminal:error', handleError);
122
+
123
+ // Send input to server
124
+ const onDataDisposable = term.onData((data) => {
125
+ emit('terminal:input', { sessionId, data });
126
+ });
127
+
128
+ // Create terminal session
129
+ emit('terminal:create', {
130
+ sessionId,
131
+ worktreePath,
132
+ agentType,
133
+ });
134
+
135
+ // Send initial resize
136
+ if (fitAddon) {
137
+ emit('terminal:resize', {
138
+ sessionId,
139
+ cols: term.cols,
140
+ rows: term.rows,
141
+ });
142
+ }
143
+
144
+ return () => {
145
+ off('terminal:output', handleOutput);
146
+ off('terminal:created', handleCreated);
147
+ off('terminal:exit', handleExit);
148
+ off('terminal:error', handleError);
149
+ onDataDisposable.dispose();
150
+ };
151
+ }, [sessionId, worktreePath, agentType, connected, emit, on, off, updateTerminalStatus]);
152
+
153
+ // Handle resize
154
+ useEffect(() => {
155
+ if (!terminalRef.current || !fitAddonRef.current) return;
156
+
157
+ const fitAddon = fitAddonRef.current;
158
+ const term = termRef.current;
159
+
160
+ const handleResize = () => {
161
+ if (isActive && term && fitAddon) {
162
+ fitAddon.fit();
163
+ emit('terminal:resize', {
164
+ sessionId,
165
+ cols: term.cols,
166
+ rows: term.rows,
167
+ });
168
+ }
169
+ };
170
+
171
+ const resizeObserver = new ResizeObserver(() => {
172
+ handleResize();
173
+ });
174
+
175
+ resizeObserver.observe(terminalRef.current);
176
+
177
+ // Also fit when becoming active
178
+ if (isActive) {
179
+ setTimeout(handleResize, 0);
180
+ }
181
+
182
+ return () => {
183
+ resizeObserver.disconnect();
184
+ };
185
+ }, [sessionId, isActive, emit]);
186
+
187
+ // Focus terminal when active
188
+ useEffect(() => {
189
+ if (isActive && termRef.current) {
190
+ termRef.current.focus();
191
+ }
192
+ }, [isActive]);
193
+
194
+ return (
195
+ <div
196
+ ref={terminalRef}
197
+ className="terminal"
198
+ style={{ height: '100%', width: '100%' }}
199
+ />
200
+ );
201
+ }
202
+
203
+ export default XTerminal;
@@ -0,0 +1,80 @@
1
+ import { useEffect, useState, useCallback, useRef } from 'react';
2
+ import { io, Socket } from 'socket.io-client';
3
+
4
+ interface SocketState {
5
+ socket: Socket | null;
6
+ connected: boolean;
7
+ }
8
+
9
+ // Singleton socket instance
10
+ let globalSocket: Socket | null = null;
11
+
12
+ export function useSocket(): SocketState & {
13
+ emit: (event: string, data: unknown) => void;
14
+ on: (event: string, callback: (data: unknown) => void) => void;
15
+ off: (event: string, callback?: (data: unknown) => void) => void;
16
+ } {
17
+ const [connected, setConnected] = useState(false);
18
+ const socketRef = useRef<Socket | null>(null);
19
+
20
+ useEffect(() => {
21
+ // Create singleton socket
22
+ if (!globalSocket) {
23
+ globalSocket = io(window.location.origin, {
24
+ reconnection: true,
25
+ reconnectionAttempts: 5,
26
+ reconnectionDelay: 1000,
27
+ });
28
+ }
29
+
30
+ socketRef.current = globalSocket;
31
+
32
+ const handleConnect = () => {
33
+ console.log('Socket connected');
34
+ setConnected(true);
35
+ };
36
+
37
+ const handleDisconnect = () => {
38
+ console.log('Socket disconnected');
39
+ setConnected(false);
40
+ };
41
+
42
+ globalSocket.on('connect', handleConnect);
43
+ globalSocket.on('disconnect', handleDisconnect);
44
+
45
+ // Check initial connection state
46
+ if (globalSocket.connected) {
47
+ setConnected(true);
48
+ }
49
+
50
+ return () => {
51
+ // Don't disconnect on unmount - keep singleton alive
52
+ globalSocket?.off('connect', handleConnect);
53
+ globalSocket?.off('disconnect', handleDisconnect);
54
+ };
55
+ }, []);
56
+
57
+ const emit = useCallback((event: string, data: unknown) => {
58
+ socketRef.current?.emit(event, data);
59
+ }, []);
60
+
61
+ const on = useCallback((event: string, callback: (data: unknown) => void) => {
62
+ socketRef.current?.on(event, callback);
63
+ }, []);
64
+
65
+ const off = useCallback((event: string, callback?: (data: unknown) => void) => {
66
+ if (callback) {
67
+ socketRef.current?.off(event, callback);
68
+ } else {
69
+ socketRef.current?.off(event);
70
+ }
71
+ }, []);
72
+
73
+ return {
74
+ socket: socketRef.current,
75
+ connected,
76
+ emit,
77
+ on,
78
+ off,
79
+ };
80
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+ import './styles/global.css';
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );