ccmanager 2.1.0 → 2.2.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.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.
@@ -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 ConfigureHooks from './ConfigureHooks.js';
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: 'hooks',
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 === 'hooks') {
41
- setView('hooks');
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('hooks');
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 === 'hooks') {
84
- return React.createElement(ConfigureHooks, { onComplete: handleSubMenuComplete });
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 });
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface ConfigureStatusHooksProps {
3
+ onComplete: () => void;
4
+ }
5
+ declare const ConfigureStatusHooks: React.FC<ConfigureStatusHooksProps>;
6
+ export default ConfigureStatusHooks;
@@ -1,4 +1,4 @@
1
- import React, { useState, useEffect } from 'react';
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 ConfigureHooks = ({ onComplete }) => {
11
+ const ConfigureStatusHooks = ({ onComplete, }) => {
12
12
  const [view, setView] = useState('menu');
13
13
  const [selectedStatus, setSelectedStatus] = useState('idle');
14
- const [hooks, setHooks] = useState({});
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 = hooks[status];
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(hooks);
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 !== 'separator') {
72
- const status = item.value;
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 = hooks[status];
72
+ const hook = statusHooks[status];
75
73
  setCurrentCommand(hook?.command || '');
76
- setCurrentEnabled(hook?.enabled ?? true); // Default to true if not set
74
+ setCurrentEnabled(hook?.enabled ?? true);
77
75
  setView('edit');
78
76
  }
79
77
  };
80
78
  const handleCommandSubmit = (value) => {
81
- setHooks(prev => ({
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 Change Hooks")),
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 Claude Code session status changes:")),
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 ConfigureHooks;
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,6 @@
1
+ import React from 'react';
2
+ interface ConfigureWorktreeHooksProps {
3
+ onComplete: () => void;
4
+ }
5
+ declare const ConfigureWorktreeHooks: React.FC<ConfigureWorktreeHooksProps>;
6
+ export default ConfigureWorktreeHooks;
@@ -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
- import * as os from 'os';
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;
@@ -4,7 +4,7 @@ import pkg from '@xterm/headless';
4
4
  import { exec } from 'child_process';
5
5
  import { promisify } from 'util';
6
6
  import { configurationManager } from './configurationManager.js';
7
- import { WorktreeService } from './worktreeService.js';
7
+ import { executeStatusHook } from '../utils/hookExecutor.js';
8
8
  import { createStateDetector } from './stateDetector.js';
9
9
  const { Terminal } = pkg;
10
10
  const execAsync = promisify(exec);
@@ -188,13 +188,13 @@ export class SessionManager extends EventEmitter {
188
188
  setupBackgroundHandler(session) {
189
189
  // Setup data handler
190
190
  this.setupDataHandler(session);
191
- // Set up interval-based state detection
192
191
  session.stateCheckInterval = setInterval(() => {
193
192
  const oldState = session.state;
194
193
  const newState = this.detectTerminalState(session);
195
194
  if (newState !== oldState) {
196
195
  session.state = newState;
197
- this.executeStatusHook(oldState, newState, session);
196
+ // Execute status hook asynchronously (non-blocking)
197
+ void executeStatusHook(oldState, newState, session);
198
198
  this.emit('sessionStateChanged', session);
199
199
  }
200
200
  }, 100); // Check every 100ms
@@ -252,36 +252,6 @@ export class SessionManager extends EventEmitter {
252
252
  getAllSessions() {
253
253
  return Array.from(this.sessions.values());
254
254
  }
255
- executeStatusHook(oldState, newState, session) {
256
- const statusHooks = configurationManager.getStatusHooks();
257
- const hook = statusHooks[newState];
258
- if (hook && hook.enabled && hook.command) {
259
- // Get branch information
260
- const worktreeService = new WorktreeService();
261
- const worktrees = worktreeService.getWorktrees();
262
- const worktree = worktrees.find(wt => wt.path === session.worktreePath);
263
- const branch = worktree?.branch || 'unknown';
264
- // Execute the hook command in the session's worktree directory
265
- exec(hook.command, {
266
- cwd: session.worktreePath,
267
- env: {
268
- ...process.env,
269
- CCMANAGER_OLD_STATE: oldState,
270
- CCMANAGER_NEW_STATE: newState,
271
- CCMANAGER_WORKTREE: session.worktreePath,
272
- CCMANAGER_WORKTREE_BRANCH: branch,
273
- CCMANAGER_SESSION_ID: session.id,
274
- },
275
- }, (error, _stdout, stderr) => {
276
- if (error) {
277
- console.error(`Failed to execute ${newState} hook: ${error.message}`);
278
- }
279
- if (stderr) {
280
- console.error(`Hook stderr: ${stderr}`);
281
- }
282
- });
283
- }
284
- }
285
255
  async createSessionWithDevcontainer(worktreePath, devcontainerConfig, presetId) {
286
256
  // Check if session already exists
287
257
  const existing = this.sessions.get(worktreePath);
@@ -10,10 +10,10 @@ export declare class WorktreeService {
10
10
  getGitRootPath(): string;
11
11
  getDefaultBranch(): string;
12
12
  getAllBranches(): string[];
13
- createWorktree(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean): {
13
+ createWorktree(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean): Promise<{
14
14
  success: boolean;
15
15
  error?: string;
16
- };
16
+ }>;
17
17
  deleteWorktree(worktreePath: string, options?: {
18
18
  deleteBranch?: boolean;
19
19
  }): {
@@ -3,6 +3,8 @@ import { existsSync, statSync, cpSync } from 'fs';
3
3
  import path from 'path';
4
4
  import { setWorktreeParentBranch } from '../utils/worktreeConfig.js';
5
5
  import { getClaudeProjectsDir, pathToClaudeProjectName, } from '../utils/claudeDir.js';
6
+ import { executeWorktreePostCreationHook } from '../utils/hookExecutor.js';
7
+ import { configurationManager } from './configurationManager.js';
6
8
  const CLAUDE_DIR = '.claude';
7
9
  export class WorktreeService {
8
10
  constructor(rootPath) {
@@ -188,7 +190,7 @@ export class WorktreeService {
188
190
  return [];
189
191
  }
190
192
  }
191
- createWorktree(worktreePath, branch, baseBranch, copySessionData = false, copyClaudeDirectory = false) {
193
+ async createWorktree(worktreePath, branch, baseBranch, copySessionData = false, copyClaudeDirectory = false) {
192
194
  try {
193
195
  // Resolve the worktree path relative to the git repository root
194
196
  const resolvedPath = path.isAbsolute(worktreePath)
@@ -239,6 +241,21 @@ export class WorktreeService {
239
241
  console.error('Warning: Failed to copy .claude directory:', error);
240
242
  }
241
243
  }
244
+ // Execute post-creation hook if configured
245
+ const worktreeHooks = configurationManager.getWorktreeHooks();
246
+ if (worktreeHooks.post_creation?.enabled &&
247
+ worktreeHooks.post_creation?.command) {
248
+ // Create a worktree object for the hook
249
+ const newWorktree = {
250
+ path: resolvedPath,
251
+ branch: branch,
252
+ isMainWorktree: false,
253
+ hasSession: false,
254
+ };
255
+ // Execute the hook synchronously (blocking)
256
+ // Wait for the hook to complete before returning
257
+ await executeWorktreePostCreationHook(worktreeHooks.post_creation.command, newWorktree, this.gitRootPath, baseBranch);
258
+ }
242
259
  return { success: true };
243
260
  }
244
261
  catch (error) {