ccmanager 2.1.0 → 2.2.1
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.md +18 -0
- package/dist/components/App.js +1 -1
- package/dist/components/Configuration.js +21 -7
- package/dist/components/ConfigureStatusHooks.d.ts +6 -0
- package/dist/components/{ConfigureHooks.js → ConfigureStatusHooks.js} +16 -18
- package/dist/components/ConfigureStatusHooks.test.d.ts +1 -0
- package/dist/components/ConfigureStatusHooks.test.js +62 -0
- package/dist/components/ConfigureWorktreeHooks.d.ts +6 -0
- package/dist/components/ConfigureWorktreeHooks.js +114 -0
- package/dist/components/ConfigureWorktreeHooks.test.d.ts +1 -0
- package/dist/components/ConfigureWorktreeHooks.test.js +60 -0
- package/dist/constants/statePersistence.d.ts +2 -0
- package/dist/constants/statePersistence.js +4 -0
- package/dist/services/configurationManager.d.ts +3 -1
- package/dist/services/configurationManager.js +10 -0
- package/dist/services/projectManager.test.js +8 -9
- package/dist/services/sessionManager.d.ts +0 -1
- package/dist/services/sessionManager.js +40 -39
- package/dist/services/sessionManager.statePersistence.test.d.ts +1 -0
- package/dist/services/sessionManager.statePersistence.test.js +215 -0
- package/dist/services/worktreeService.d.ts +2 -2
- package/dist/services/worktreeService.js +18 -1
- package/dist/services/worktreeService.test.js +162 -7
- package/dist/types/index.d.ts +17 -7
- package/dist/utils/hookExecutor.d.ts +20 -0
- package/dist/utils/hookExecutor.js +96 -0
- package/dist/utils/hookExecutor.test.d.ts +1 -0
- package/dist/utils/hookExecutor.test.js +405 -0
- package/dist/utils/worktreeUtils.test.js +7 -0
- package/package.json +1 -1
- package/dist/components/ConfigureHooks.d.ts +0 -6
package/README.md
CHANGED
|
@@ -205,6 +205,24 @@ Status hooks allow you to:
|
|
|
205
205
|
|
|
206
206
|
For detailed setup instructions, see [docs/state-hooks.md](docs/status-hooks.md).
|
|
207
207
|
|
|
208
|
+
## Worktree Hooks
|
|
209
|
+
|
|
210
|
+
Worktree hooks execute custom commands when worktrees are created, enabling automation of development environment setup.
|
|
211
|
+
|
|
212
|
+
### Features
|
|
213
|
+
- **Post-creation hook**: Run commands after a worktree is created
|
|
214
|
+
- **Environment variables**: Access worktree path, branch name, and git root
|
|
215
|
+
- **Non-blocking execution**: Hooks run asynchronously without delaying operations
|
|
216
|
+
- **Error resilience**: Hook failures don't prevent worktree creation
|
|
217
|
+
|
|
218
|
+
### Use Cases
|
|
219
|
+
- Set up development dependencies (`npm install`, `bundle install`)
|
|
220
|
+
- Configure IDE settings per branch
|
|
221
|
+
- Send notifications when worktrees are created
|
|
222
|
+
- Initialize branch-specific configurations
|
|
223
|
+
|
|
224
|
+
For configuration and examples, see [docs/worktree-hooks.md](docs/worktree-hooks.md).
|
|
225
|
+
|
|
208
226
|
## Automatic Worktree Directory Generation
|
|
209
227
|
|
|
210
228
|
CCManager can automatically generate worktree directory paths based on branch names, streamlining the worktree creation process.
|
package/dist/components/App.js
CHANGED
|
@@ -183,7 +183,7 @@ const App = ({ devcontainerConfig, multiProject }) => {
|
|
|
183
183
|
setView('creating-worktree');
|
|
184
184
|
setError(null);
|
|
185
185
|
// Create the worktree
|
|
186
|
-
const result = worktreeService.createWorktree(path, branch, baseBranch, copySessionData, copyClaudeDirectory);
|
|
186
|
+
const result = await worktreeService.createWorktree(path, branch, baseBranch, copySessionData, copyClaudeDirectory);
|
|
187
187
|
if (result.success) {
|
|
188
188
|
// Success - return to menu
|
|
189
189
|
handleReturnToMenu();
|
|
@@ -2,7 +2,8 @@ import React, { useState } from 'react';
|
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
3
|
import SelectInput from 'ink-select-input';
|
|
4
4
|
import ConfigureShortcuts from './ConfigureShortcuts.js';
|
|
5
|
-
import
|
|
5
|
+
import ConfigureStatusHooks from './ConfigureStatusHooks.js';
|
|
6
|
+
import ConfigureWorktreeHooks from './ConfigureWorktreeHooks.js';
|
|
6
7
|
import ConfigureWorktree from './ConfigureWorktree.js';
|
|
7
8
|
import ConfigureCommand from './ConfigureCommand.js';
|
|
8
9
|
import { shortcutManager } from '../services/shortcutManager.js';
|
|
@@ -15,7 +16,11 @@ const Configuration = ({ onComplete }) => {
|
|
|
15
16
|
},
|
|
16
17
|
{
|
|
17
18
|
label: 'H 🔧 Configure Status Hooks',
|
|
18
|
-
value: '
|
|
19
|
+
value: 'statusHooks',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
label: 'T 🔨 Configure Worktree Hooks',
|
|
23
|
+
value: 'worktreeHooks',
|
|
19
24
|
},
|
|
20
25
|
{
|
|
21
26
|
label: 'W 📁 Configure Worktree Settings',
|
|
@@ -37,8 +42,11 @@ const Configuration = ({ onComplete }) => {
|
|
|
37
42
|
else if (item.value === 'shortcuts') {
|
|
38
43
|
setView('shortcuts');
|
|
39
44
|
}
|
|
40
|
-
else if (item.value === '
|
|
41
|
-
setView('
|
|
45
|
+
else if (item.value === 'statusHooks') {
|
|
46
|
+
setView('statusHooks');
|
|
47
|
+
}
|
|
48
|
+
else if (item.value === 'worktreeHooks') {
|
|
49
|
+
setView('worktreeHooks');
|
|
42
50
|
}
|
|
43
51
|
else if (item.value === 'worktree') {
|
|
44
52
|
setView('worktree');
|
|
@@ -60,7 +68,10 @@ const Configuration = ({ onComplete }) => {
|
|
|
60
68
|
setView('shortcuts');
|
|
61
69
|
break;
|
|
62
70
|
case 'h':
|
|
63
|
-
setView('
|
|
71
|
+
setView('statusHooks');
|
|
72
|
+
break;
|
|
73
|
+
case 't':
|
|
74
|
+
setView('worktreeHooks');
|
|
64
75
|
break;
|
|
65
76
|
case 'w':
|
|
66
77
|
setView('worktree');
|
|
@@ -80,8 +91,11 @@ const Configuration = ({ onComplete }) => {
|
|
|
80
91
|
if (view === 'shortcuts') {
|
|
81
92
|
return React.createElement(ConfigureShortcuts, { onComplete: handleSubMenuComplete });
|
|
82
93
|
}
|
|
83
|
-
if (view === '
|
|
84
|
-
return React.createElement(
|
|
94
|
+
if (view === 'statusHooks') {
|
|
95
|
+
return React.createElement(ConfigureStatusHooks, { onComplete: handleSubMenuComplete });
|
|
96
|
+
}
|
|
97
|
+
if (view === 'worktreeHooks') {
|
|
98
|
+
return React.createElement(ConfigureWorktreeHooks, { onComplete: handleSubMenuComplete });
|
|
85
99
|
}
|
|
86
100
|
if (view === 'worktree') {
|
|
87
101
|
return React.createElement(ConfigureWorktree, { onComplete: handleSubMenuComplete });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import { Box, Text, useInput } from 'ink';
|
|
3
3
|
import TextInputWrapper from './TextInputWrapper.js';
|
|
4
4
|
import SelectInput from 'ink-select-input';
|
|
@@ -8,16 +8,13 @@ const STATUS_LABELS = {
|
|
|
8
8
|
busy: 'Busy',
|
|
9
9
|
waiting_input: 'Waiting for Input',
|
|
10
10
|
};
|
|
11
|
-
const
|
|
11
|
+
const ConfigureStatusHooks = ({ onComplete, }) => {
|
|
12
12
|
const [view, setView] = useState('menu');
|
|
13
13
|
const [selectedStatus, setSelectedStatus] = useState('idle');
|
|
14
|
-
const [
|
|
14
|
+
const [statusHooks, setStatusHooks] = useState(configurationManager.getStatusHooks());
|
|
15
15
|
const [currentCommand, setCurrentCommand] = useState('');
|
|
16
16
|
const [currentEnabled, setCurrentEnabled] = useState(false);
|
|
17
17
|
const [showSaveMessage, setShowSaveMessage] = useState(false);
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
setHooks(configurationManager.getStatusHooks());
|
|
20
|
-
}, []);
|
|
21
18
|
useInput((input, key) => {
|
|
22
19
|
if (key.escape) {
|
|
23
20
|
if (view === 'edit') {
|
|
@@ -35,16 +32,16 @@ const ConfigureHooks = ({ onComplete }) => {
|
|
|
35
32
|
const items = [];
|
|
36
33
|
// Add status hook items
|
|
37
34
|
['idle', 'busy', 'waiting_input'].forEach(status => {
|
|
38
|
-
const hook =
|
|
35
|
+
const hook = statusHooks[status];
|
|
39
36
|
const enabled = hook?.enabled ? '✓' : '✗';
|
|
40
37
|
const command = hook?.command || '(not set)';
|
|
41
38
|
items.push({
|
|
42
39
|
label: `${STATUS_LABELS[status]}: ${enabled} ${command}`,
|
|
43
|
-
value: status
|
|
40
|
+
value: `status:${status}`,
|
|
44
41
|
});
|
|
45
42
|
});
|
|
46
43
|
items.push({
|
|
47
|
-
label: '
|
|
44
|
+
label: '',
|
|
48
45
|
value: 'separator',
|
|
49
46
|
});
|
|
50
47
|
items.push({
|
|
@@ -59,7 +56,7 @@ const ConfigureHooks = ({ onComplete }) => {
|
|
|
59
56
|
};
|
|
60
57
|
const handleMenuSelect = (item) => {
|
|
61
58
|
if (item.value === 'save') {
|
|
62
|
-
configurationManager.setStatusHooks(
|
|
59
|
+
configurationManager.setStatusHooks(statusHooks);
|
|
63
60
|
setShowSaveMessage(true);
|
|
64
61
|
setTimeout(() => {
|
|
65
62
|
onComplete();
|
|
@@ -68,17 +65,18 @@ const ConfigureHooks = ({ onComplete }) => {
|
|
|
68
65
|
else if (item.value === 'cancel') {
|
|
69
66
|
onComplete();
|
|
70
67
|
}
|
|
71
|
-
else if (item.value
|
|
72
|
-
|
|
68
|
+
else if (!item.value.includes('separator') &&
|
|
69
|
+
item.value.startsWith('status:')) {
|
|
70
|
+
const status = item.value.split(':')[1];
|
|
73
71
|
setSelectedStatus(status);
|
|
74
|
-
const hook =
|
|
72
|
+
const hook = statusHooks[status];
|
|
75
73
|
setCurrentCommand(hook?.command || '');
|
|
76
|
-
setCurrentEnabled(hook?.enabled ?? true);
|
|
74
|
+
setCurrentEnabled(hook?.enabled ?? true);
|
|
77
75
|
setView('edit');
|
|
78
76
|
}
|
|
79
77
|
};
|
|
80
78
|
const handleCommandSubmit = (value) => {
|
|
81
|
-
|
|
79
|
+
setStatusHooks(prev => ({
|
|
82
80
|
...prev,
|
|
83
81
|
[selectedStatus]: {
|
|
84
82
|
command: value,
|
|
@@ -123,11 +121,11 @@ const ConfigureHooks = ({ onComplete }) => {
|
|
|
123
121
|
}
|
|
124
122
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
125
123
|
React.createElement(Box, { marginBottom: 1 },
|
|
126
|
-
React.createElement(Text, { bold: true, color: "green" }, "Configure Status
|
|
124
|
+
React.createElement(Text, { bold: true, color: "green" }, "Configure Status Hooks")),
|
|
127
125
|
React.createElement(Box, { marginBottom: 1 },
|
|
128
|
-
React.createElement(Text, { dimColor: true }, "Set commands to run when
|
|
126
|
+
React.createElement(Text, { dimColor: true }, "Set commands to run when session status changes:")),
|
|
129
127
|
React.createElement(SelectInput, { items: getMenuItems(), onSelect: handleMenuSelect, isFocused: true, limit: 10 }),
|
|
130
128
|
React.createElement(Box, { marginTop: 1 },
|
|
131
129
|
React.createElement(Text, { dimColor: true }, "Press Esc to go back"))));
|
|
132
130
|
};
|
|
133
|
-
export default
|
|
131
|
+
export default ConfigureStatusHooks;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'ink-testing-library';
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
+
import ConfigureStatusHooks from './ConfigureStatusHooks.js';
|
|
5
|
+
import { configurationManager } from '../services/configurationManager.js';
|
|
6
|
+
// Mock ink to avoid stdin issues
|
|
7
|
+
vi.mock('ink', async () => {
|
|
8
|
+
const actual = await vi.importActual('ink');
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
useInput: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
// Mock SelectInput to render items as simple text
|
|
15
|
+
vi.mock('ink-select-input', async () => {
|
|
16
|
+
const React = await vi.importActual('react');
|
|
17
|
+
const { Text, Box } = await vi.importActual('ink');
|
|
18
|
+
return {
|
|
19
|
+
default: ({ items }) => {
|
|
20
|
+
return React.createElement(Box, { flexDirection: 'column' }, items.map((item, index) => React.createElement(Text, { key: index }, item.label)));
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
vi.mock('../services/configurationManager.js', () => ({
|
|
25
|
+
configurationManager: {
|
|
26
|
+
getStatusHooks: vi.fn(),
|
|
27
|
+
setStatusHooks: vi.fn(),
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
const mockedConfigurationManager = configurationManager;
|
|
31
|
+
describe('ConfigureStatusHooks', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
it('should render status hooks configuration screen', () => {
|
|
36
|
+
mockedConfigurationManager.getStatusHooks.mockReturnValue({});
|
|
37
|
+
const onComplete = vi.fn();
|
|
38
|
+
const { lastFrame } = render(React.createElement(ConfigureStatusHooks, { onComplete: onComplete }));
|
|
39
|
+
expect(lastFrame()).toContain('Configure Status Hooks');
|
|
40
|
+
expect(lastFrame()).toContain('Set commands to run when session status changes');
|
|
41
|
+
expect(lastFrame()).toContain('Idle:');
|
|
42
|
+
expect(lastFrame()).toContain('Busy:');
|
|
43
|
+
expect(lastFrame()).toContain('Waiting for Input:');
|
|
44
|
+
});
|
|
45
|
+
it('should display configured hooks', () => {
|
|
46
|
+
mockedConfigurationManager.getStatusHooks.mockReturnValue({
|
|
47
|
+
idle: {
|
|
48
|
+
command: 'notify-send "Idle"',
|
|
49
|
+
enabled: true,
|
|
50
|
+
},
|
|
51
|
+
busy: {
|
|
52
|
+
command: 'echo "Busy"',
|
|
53
|
+
enabled: false,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const onComplete = vi.fn();
|
|
57
|
+
const { lastFrame } = render(React.createElement(ConfigureStatusHooks, { onComplete: onComplete }));
|
|
58
|
+
expect(lastFrame()).toContain('Idle: ✓ notify-send "Idle"');
|
|
59
|
+
expect(lastFrame()).toContain('Busy: ✗ echo "Busy"');
|
|
60
|
+
expect(lastFrame()).toContain('Waiting for Input: ✗ (not set)');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import TextInputWrapper from './TextInputWrapper.js';
|
|
4
|
+
import SelectInput from 'ink-select-input';
|
|
5
|
+
import { configurationManager } from '../services/configurationManager.js';
|
|
6
|
+
const ConfigureWorktreeHooks = ({ onComplete, }) => {
|
|
7
|
+
const [view, setView] = useState('menu');
|
|
8
|
+
const [worktreeHooks, setWorktreeHooks] = useState(configurationManager.getWorktreeHooks());
|
|
9
|
+
const [currentCommand, setCurrentCommand] = useState('');
|
|
10
|
+
const [currentEnabled, setCurrentEnabled] = useState(false);
|
|
11
|
+
const [showSaveMessage, setShowSaveMessage] = useState(false);
|
|
12
|
+
useInput((input, key) => {
|
|
13
|
+
if (key.escape) {
|
|
14
|
+
if (view === 'edit') {
|
|
15
|
+
setView('menu');
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
onComplete();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else if (key.tab && view === 'edit') {
|
|
22
|
+
toggleEnabled();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const getMenuItems = () => {
|
|
26
|
+
const items = [];
|
|
27
|
+
// Add worktree hook items
|
|
28
|
+
const postCreationHook = worktreeHooks.post_creation;
|
|
29
|
+
const postCreationEnabled = postCreationHook?.enabled ? '✓' : '✗';
|
|
30
|
+
const postCreationCommand = postCreationHook?.command || '(not set)';
|
|
31
|
+
items.push({
|
|
32
|
+
label: `Post Creation: ${postCreationEnabled} ${postCreationCommand}`,
|
|
33
|
+
value: 'worktree:post_creation',
|
|
34
|
+
});
|
|
35
|
+
items.push({
|
|
36
|
+
label: '',
|
|
37
|
+
value: 'separator',
|
|
38
|
+
});
|
|
39
|
+
items.push({
|
|
40
|
+
label: '💾 Save and Return',
|
|
41
|
+
value: 'save',
|
|
42
|
+
});
|
|
43
|
+
items.push({
|
|
44
|
+
label: '← Cancel',
|
|
45
|
+
value: 'cancel',
|
|
46
|
+
});
|
|
47
|
+
return items;
|
|
48
|
+
};
|
|
49
|
+
const handleMenuSelect = (item) => {
|
|
50
|
+
if (item.value === 'save') {
|
|
51
|
+
configurationManager.setWorktreeHooks(worktreeHooks);
|
|
52
|
+
setShowSaveMessage(true);
|
|
53
|
+
setTimeout(() => {
|
|
54
|
+
onComplete();
|
|
55
|
+
}, 1000);
|
|
56
|
+
}
|
|
57
|
+
else if (item.value === 'cancel') {
|
|
58
|
+
onComplete();
|
|
59
|
+
}
|
|
60
|
+
else if (!item.value.includes('separator') &&
|
|
61
|
+
item.value === 'worktree:post_creation') {
|
|
62
|
+
const hook = worktreeHooks.post_creation;
|
|
63
|
+
setCurrentCommand(hook?.command || '');
|
|
64
|
+
setCurrentEnabled(hook?.enabled ?? true);
|
|
65
|
+
setView('edit');
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const handleCommandSubmit = (value) => {
|
|
69
|
+
setWorktreeHooks(prev => ({
|
|
70
|
+
...prev,
|
|
71
|
+
post_creation: {
|
|
72
|
+
command: value,
|
|
73
|
+
enabled: currentEnabled,
|
|
74
|
+
},
|
|
75
|
+
}));
|
|
76
|
+
setView('menu');
|
|
77
|
+
};
|
|
78
|
+
const toggleEnabled = () => {
|
|
79
|
+
setCurrentEnabled(prev => !prev);
|
|
80
|
+
};
|
|
81
|
+
if (showSaveMessage) {
|
|
82
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
83
|
+
React.createElement(Text, { color: "green" }, "\u2713 Configuration saved successfully!")));
|
|
84
|
+
}
|
|
85
|
+
if (view === 'edit') {
|
|
86
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
87
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
88
|
+
React.createElement(Text, { bold: true, color: "green" }, "Configure Post Worktree Creation Hook")),
|
|
89
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
90
|
+
React.createElement(Text, null, "Command to execute after creating a new worktree:")),
|
|
91
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
92
|
+
React.createElement(TextInputWrapper, { value: currentCommand, onChange: setCurrentCommand, onSubmit: handleCommandSubmit, placeholder: "Enter command (e.g., npm install && npm run build)" })),
|
|
93
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
94
|
+
React.createElement(Text, null,
|
|
95
|
+
"Enabled: ",
|
|
96
|
+
currentEnabled ? '✓' : '✗',
|
|
97
|
+
" (Press Tab to toggle)")),
|
|
98
|
+
React.createElement(Box, { marginTop: 1 },
|
|
99
|
+
React.createElement(Text, { dimColor: true }, "Environment variables available: CCMANAGER_WORKTREE, CCMANAGER_WORKTREE_BRANCH,")),
|
|
100
|
+
React.createElement(Box, null,
|
|
101
|
+
React.createElement(Text, { dimColor: true }, "CCMANAGER_BASE_BRANCH, CCMANAGER_GIT_ROOT")),
|
|
102
|
+
React.createElement(Box, { marginTop: 1 },
|
|
103
|
+
React.createElement(Text, { dimColor: true }, "Press Enter to save, Tab to toggle enabled, Esc to cancel"))));
|
|
104
|
+
}
|
|
105
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
106
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
107
|
+
React.createElement(Text, { bold: true, color: "green" }, "Configure Worktree Hooks")),
|
|
108
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
109
|
+
React.createElement(Text, { dimColor: true }, "Set commands to run on worktree events:")),
|
|
110
|
+
React.createElement(SelectInput, { items: getMenuItems(), onSelect: handleMenuSelect, isFocused: true, limit: 10 }),
|
|
111
|
+
React.createElement(Box, { marginTop: 1 },
|
|
112
|
+
React.createElement(Text, { dimColor: true }, "Press Esc to go back"))));
|
|
113
|
+
};
|
|
114
|
+
export default ConfigureWorktreeHooks;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'ink-testing-library';
|
|
3
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
+
import ConfigureWorktreeHooks from './ConfigureWorktreeHooks.js';
|
|
5
|
+
import { configurationManager } from '../services/configurationManager.js';
|
|
6
|
+
// Mock ink to avoid stdin issues
|
|
7
|
+
vi.mock('ink', async () => {
|
|
8
|
+
const actual = await vi.importActual('ink');
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
useInput: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
// Mock SelectInput to render items as simple text
|
|
15
|
+
vi.mock('ink-select-input', async () => {
|
|
16
|
+
const React = await vi.importActual('react');
|
|
17
|
+
const { Text, Box } = await vi.importActual('ink');
|
|
18
|
+
return {
|
|
19
|
+
default: ({ items }) => {
|
|
20
|
+
return React.createElement(Box, { flexDirection: 'column' }, items.map((item, index) => React.createElement(Text, { key: index }, item.label)));
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
vi.mock('../services/configurationManager.js', () => ({
|
|
25
|
+
configurationManager: {
|
|
26
|
+
getWorktreeHooks: vi.fn(),
|
|
27
|
+
setWorktreeHooks: vi.fn(),
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
const mockedConfigurationManager = configurationManager;
|
|
31
|
+
describe('ConfigureWorktreeHooks', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
it('should render worktree hooks configuration screen', () => {
|
|
36
|
+
mockedConfigurationManager.getWorktreeHooks.mockReturnValue({});
|
|
37
|
+
const onComplete = vi.fn();
|
|
38
|
+
const { lastFrame } = render(React.createElement(ConfigureWorktreeHooks, { onComplete: onComplete }));
|
|
39
|
+
expect(lastFrame()).toContain('Configure Worktree Hooks');
|
|
40
|
+
expect(lastFrame()).toContain('Set commands to run on worktree events');
|
|
41
|
+
expect(lastFrame()).toContain('Post Creation:');
|
|
42
|
+
});
|
|
43
|
+
it('should display configured hooks', () => {
|
|
44
|
+
mockedConfigurationManager.getWorktreeHooks.mockReturnValue({
|
|
45
|
+
post_creation: {
|
|
46
|
+
command: 'npm install',
|
|
47
|
+
enabled: true,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const onComplete = vi.fn();
|
|
51
|
+
const { lastFrame } = render(React.createElement(ConfigureWorktreeHooks, { onComplete: onComplete }));
|
|
52
|
+
expect(lastFrame()).toContain('Post Creation: ✓ npm install');
|
|
53
|
+
});
|
|
54
|
+
it('should display not set when no hook configured', () => {
|
|
55
|
+
mockedConfigurationManager.getWorktreeHooks.mockReturnValue({});
|
|
56
|
+
const onComplete = vi.fn();
|
|
57
|
+
const { lastFrame } = render(React.createElement(ConfigureWorktreeHooks, { onComplete: onComplete }));
|
|
58
|
+
expect(lastFrame()).toContain('Post Creation: ✗ (not set)');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConfigurationData, StatusHookConfig, ShortcutConfig, WorktreeConfig, CommandConfig, CommandPreset, CommandPresetsConfig } from '../types/index.js';
|
|
1
|
+
import { ConfigurationData, StatusHookConfig, WorktreeHookConfig, ShortcutConfig, WorktreeConfig, CommandConfig, CommandPreset, CommandPresetsConfig } from '../types/index.js';
|
|
2
2
|
export declare class ConfigurationManager {
|
|
3
3
|
private configPath;
|
|
4
4
|
private legacyShortcutsPath;
|
|
@@ -12,6 +12,8 @@ export declare class ConfigurationManager {
|
|
|
12
12
|
setShortcuts(shortcuts: ShortcutConfig): void;
|
|
13
13
|
getStatusHooks(): StatusHookConfig;
|
|
14
14
|
setStatusHooks(hooks: StatusHookConfig): void;
|
|
15
|
+
getWorktreeHooks(): WorktreeHookConfig;
|
|
16
|
+
setWorktreeHooks(hooks: WorktreeHookConfig): void;
|
|
15
17
|
getConfiguration(): ConfigurationData;
|
|
16
18
|
setConfiguration(config: ConfigurationData): void;
|
|
17
19
|
getWorktreeConfig(): WorktreeConfig;
|
|
@@ -70,6 +70,9 @@ export class ConfigurationManager {
|
|
|
70
70
|
if (!this.config.statusHooks) {
|
|
71
71
|
this.config.statusHooks = {};
|
|
72
72
|
}
|
|
73
|
+
if (!this.config.worktreeHooks) {
|
|
74
|
+
this.config.worktreeHooks = {};
|
|
75
|
+
}
|
|
73
76
|
if (!this.config.worktree) {
|
|
74
77
|
this.config.worktree = {
|
|
75
78
|
autoDirectory: false,
|
|
@@ -127,6 +130,13 @@ export class ConfigurationManager {
|
|
|
127
130
|
this.config.statusHooks = hooks;
|
|
128
131
|
this.saveConfig();
|
|
129
132
|
}
|
|
133
|
+
getWorktreeHooks() {
|
|
134
|
+
return this.config.worktreeHooks || {};
|
|
135
|
+
}
|
|
136
|
+
setWorktreeHooks(hooks) {
|
|
137
|
+
this.config.worktreeHooks = hooks;
|
|
138
|
+
this.saveConfig();
|
|
139
|
+
}
|
|
130
140
|
getConfiguration() {
|
|
131
141
|
return this.config;
|
|
132
142
|
}
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
-
import { ProjectManager } from './projectManager.js';
|
|
3
|
-
import { ENV_VARS } from '../constants/env.js';
|
|
4
2
|
import * as fs from 'fs';
|
|
5
3
|
import * as path from 'path';
|
|
6
|
-
|
|
7
|
-
// Mock fs module
|
|
4
|
+
// Mock modules before any other imports that might use them
|
|
8
5
|
vi.mock('fs');
|
|
9
|
-
vi.mock('os')
|
|
6
|
+
vi.mock('os', () => ({
|
|
7
|
+
homedir: vi.fn(() => '/home/user'),
|
|
8
|
+
platform: vi.fn(() => 'linux'),
|
|
9
|
+
}));
|
|
10
|
+
// Now import modules that depend on the mocked modules
|
|
11
|
+
import { ProjectManager } from './projectManager.js';
|
|
12
|
+
import { ENV_VARS } from '../constants/env.js';
|
|
10
13
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
14
|
const mockFs = fs;
|
|
12
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
-
const mockOs = os;
|
|
14
15
|
describe('ProjectManager', () => {
|
|
15
16
|
let projectManager;
|
|
16
17
|
const mockProjectsDir = '/home/user/projects';
|
|
@@ -20,8 +21,6 @@ describe('ProjectManager', () => {
|
|
|
20
21
|
vi.clearAllMocks();
|
|
21
22
|
// Reset environment variables
|
|
22
23
|
delete process.env[ENV_VARS.MULTI_PROJECT_ROOT];
|
|
23
|
-
// Mock os.homedir
|
|
24
|
-
mockOs.homedir.mockReturnValue('/home/user');
|
|
25
24
|
// Mock fs methods for config directory
|
|
26
25
|
mockFs.existsSync.mockImplementation((path) => {
|
|
27
26
|
if (path === mockConfigDir)
|
|
@@ -32,7 +32,6 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
|
|
|
32
32
|
setSessionActive(worktreePath: string, active: boolean): void;
|
|
33
33
|
destroySession(worktreePath: string): void;
|
|
34
34
|
getAllSessions(): Session[];
|
|
35
|
-
private executeStatusHook;
|
|
36
35
|
createSessionWithDevcontainer(worktreePath: string, devcontainerConfig: DevcontainerConfig, presetId?: string): Promise<Session>;
|
|
37
36
|
destroy(): void;
|
|
38
37
|
static getSessionCounts(sessions: Session[]): SessionCounts;
|