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.
Files changed (31) hide show
  1. package/README.md +18 -0
  2. package/dist/components/App.js +1 -1
  3. package/dist/components/Configuration.js +21 -7
  4. package/dist/components/ConfigureStatusHooks.d.ts +6 -0
  5. package/dist/components/{ConfigureHooks.js → ConfigureStatusHooks.js} +16 -18
  6. package/dist/components/ConfigureStatusHooks.test.d.ts +1 -0
  7. package/dist/components/ConfigureStatusHooks.test.js +62 -0
  8. package/dist/components/ConfigureWorktreeHooks.d.ts +6 -0
  9. package/dist/components/ConfigureWorktreeHooks.js +114 -0
  10. package/dist/components/ConfigureWorktreeHooks.test.d.ts +1 -0
  11. package/dist/components/ConfigureWorktreeHooks.test.js +60 -0
  12. package/dist/constants/statePersistence.d.ts +2 -0
  13. package/dist/constants/statePersistence.js +4 -0
  14. package/dist/services/configurationManager.d.ts +3 -1
  15. package/dist/services/configurationManager.js +10 -0
  16. package/dist/services/projectManager.test.js +8 -9
  17. package/dist/services/sessionManager.d.ts +0 -1
  18. package/dist/services/sessionManager.js +40 -39
  19. package/dist/services/sessionManager.statePersistence.test.d.ts +1 -0
  20. package/dist/services/sessionManager.statePersistence.test.js +215 -0
  21. package/dist/services/worktreeService.d.ts +2 -2
  22. package/dist/services/worktreeService.js +18 -1
  23. package/dist/services/worktreeService.test.js +162 -7
  24. package/dist/types/index.d.ts +17 -7
  25. package/dist/utils/hookExecutor.d.ts +20 -0
  26. package/dist/utils/hookExecutor.js +96 -0
  27. package/dist/utils/hookExecutor.test.d.ts +1 -0
  28. package/dist/utils/hookExecutor.test.js +405 -0
  29. package/dist/utils/worktreeUtils.test.js +7 -0
  30. package/package.json +1 -1
  31. package/dist/components/ConfigureHooks.d.ts +0 -6
@@ -22,11 +22,13 @@ export interface Session {
22
22
  lastActivity: Date;
23
23
  isActive: boolean;
24
24
  terminal: Terminal;
25
- stateCheckInterval?: NodeJS.Timeout;
26
- isPrimaryCommand?: boolean;
27
- commandConfig?: CommandConfig;
28
- detectionStrategy?: StateDetectionStrategy;
29
- devcontainerConfig?: DevcontainerConfig;
25
+ stateCheckInterval: NodeJS.Timeout | undefined;
26
+ isPrimaryCommand: boolean;
27
+ commandConfig: CommandConfig | undefined;
28
+ detectionStrategy: StateDetectionStrategy | undefined;
29
+ devcontainerConfig: DevcontainerConfig | undefined;
30
+ pendingState: SessionState | undefined;
31
+ pendingStateStart: number | undefined;
30
32
  }
31
33
  export interface SessionManager {
32
34
  sessions: Map<string, Session>;
@@ -54,6 +56,13 @@ export interface StatusHookConfig {
54
56
  busy?: StatusHook;
55
57
  waiting_input?: StatusHook;
56
58
  }
59
+ export interface WorktreeHook {
60
+ command: string;
61
+ enabled: boolean;
62
+ }
63
+ export interface WorktreeHookConfig {
64
+ post_creation?: WorktreeHook;
65
+ }
57
66
  export interface WorktreeConfig {
58
67
  autoDirectory: boolean;
59
68
  autoDirectoryPattern?: string;
@@ -84,6 +93,7 @@ export interface DevcontainerConfig {
84
93
  export interface ConfigurationData {
85
94
  shortcuts?: ShortcutConfig;
86
95
  statusHooks?: StatusHookConfig;
96
+ worktreeHooks?: WorktreeHookConfig;
87
97
  worktree?: WorktreeConfig;
88
98
  command?: CommandConfig;
89
99
  commandPresets?: CommandPresetsConfig;
@@ -126,10 +136,10 @@ export interface IProjectManager {
126
136
  export interface IWorktreeService {
127
137
  getWorktrees(): Worktree[];
128
138
  getGitRootPath(): string;
129
- createWorktree(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean): {
139
+ createWorktree(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean): Promise<{
130
140
  success: boolean;
131
141
  error?: string;
132
- };
142
+ }>;
133
143
  deleteWorktree(worktreePath: string, options?: {
134
144
  deleteBranch?: boolean;
135
145
  }): {
@@ -0,0 +1,20 @@
1
+ import { Worktree, Session, SessionState } from '../types/index.js';
2
+ export interface HookEnvironment {
3
+ CCMANAGER_WORKTREE_PATH: string;
4
+ CCMANAGER_WORKTREE_BRANCH: string;
5
+ CCMANAGER_GIT_ROOT: string;
6
+ CCMANAGER_BASE_BRANCH?: string;
7
+ [key: string]: string | undefined;
8
+ }
9
+ /**
10
+ * Execute a hook command with the provided environment variables
11
+ */
12
+ export declare function executeHook(command: string, cwd: string, environment: HookEnvironment): Promise<void>;
13
+ /**
14
+ * Execute a worktree post-creation hook
15
+ */
16
+ export declare function executeWorktreePostCreationHook(command: string, worktree: Worktree, gitRoot: string, baseBranch?: string): Promise<void>;
17
+ /**
18
+ * Execute a session status change hook
19
+ */
20
+ export declare function executeStatusHook(oldState: SessionState, newState: SessionState, session: Session): Promise<void>;
@@ -0,0 +1,96 @@
1
+ import { spawn } from 'child_process';
2
+ import { WorktreeService } from '../services/worktreeService.js';
3
+ import { configurationManager } from '../services/configurationManager.js';
4
+ /**
5
+ * Execute a hook command with the provided environment variables
6
+ */
7
+ export function executeHook(command, cwd, environment) {
8
+ return new Promise((resolve, reject) => {
9
+ // Use spawn with shell to execute the command and wait for all child processes
10
+ const child = spawn(command, [], {
11
+ cwd,
12
+ env: {
13
+ ...process.env,
14
+ ...environment,
15
+ },
16
+ shell: true,
17
+ stdio: ['ignore', 'pipe', 'pipe'],
18
+ });
19
+ let stderr = '';
20
+ // Collect stderr for logging
21
+ child.stderr?.on('data', data => {
22
+ stderr += data.toString();
23
+ });
24
+ // Wait for the process and all its children to exit
25
+ child.on('exit', (code, signal) => {
26
+ if (code !== 0 || signal) {
27
+ let errorMessage = signal
28
+ ? `Hook terminated by signal ${signal}`
29
+ : `Hook exited with code ${code}`;
30
+ // Include stderr in the error message when exit code is not 0
31
+ if (stderr) {
32
+ errorMessage += `\nStderr: ${stderr}`;
33
+ }
34
+ reject(new Error(errorMessage));
35
+ return;
36
+ }
37
+ // When exit code is 0, ignore stderr and resolve successfully
38
+ resolve();
39
+ });
40
+ // Handle errors in spawning the process
41
+ child.on('error', error => {
42
+ reject(error);
43
+ });
44
+ });
45
+ }
46
+ /**
47
+ * Execute a worktree post-creation hook
48
+ */
49
+ export async function executeWorktreePostCreationHook(command, worktree, gitRoot, baseBranch) {
50
+ const environment = {
51
+ CCMANAGER_WORKTREE_PATH: worktree.path,
52
+ CCMANAGER_WORKTREE_BRANCH: worktree.branch || 'unknown',
53
+ CCMANAGER_GIT_ROOT: gitRoot,
54
+ };
55
+ if (baseBranch) {
56
+ environment.CCMANAGER_BASE_BRANCH = baseBranch;
57
+ }
58
+ try {
59
+ await executeHook(command, worktree.path, environment);
60
+ }
61
+ catch (error) {
62
+ // Log error but don't throw - hooks should not break the main flow
63
+ console.error(`Failed to execute post-creation hook: ${error instanceof Error ? error.message : String(error)}`);
64
+ }
65
+ }
66
+ /**
67
+ * Execute a session status change hook
68
+ */
69
+ export async function executeStatusHook(oldState, newState, session) {
70
+ const statusHooks = configurationManager.getStatusHooks();
71
+ const hook = statusHooks[newState];
72
+ if (hook && hook.enabled && hook.command) {
73
+ // Get branch information
74
+ const worktreeService = new WorktreeService();
75
+ const worktrees = worktreeService.getWorktrees();
76
+ const worktree = worktrees.find(wt => wt.path === session.worktreePath);
77
+ const branch = worktree?.branch || 'unknown';
78
+ // Build environment for status hook
79
+ const environment = {
80
+ CCMANAGER_WORKTREE_PATH: session.worktreePath,
81
+ CCMANAGER_WORKTREE_BRANCH: branch,
82
+ CCMANAGER_GIT_ROOT: session.worktreePath, // For status hooks, we use worktree path as cwd
83
+ CCMANAGER_OLD_STATE: oldState,
84
+ CCMANAGER_NEW_STATE: newState,
85
+ CCMANAGER_SESSION_ID: session.id,
86
+ };
87
+ // Execute the hook command in the session's worktree directory
88
+ try {
89
+ await executeHook(hook.command, session.worktreePath, environment);
90
+ }
91
+ catch (error) {
92
+ // Log error but don't throw - hooks should not break the main flow
93
+ console.error(`Failed to execute ${newState} hook: ${error instanceof Error ? error.message : String(error)}`);
94
+ }
95
+ }
96
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,405 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { executeHook, executeWorktreePostCreationHook, executeStatusHook, } from './hookExecutor.js';
3
+ import { mkdtemp, rm, readFile } from 'fs/promises';
4
+ import { tmpdir } from 'os';
5
+ import { join } from 'path';
6
+ import { configurationManager } from '../services/configurationManager.js';
7
+ import { WorktreeService } from '../services/worktreeService.js';
8
+ // Mock the configurationManager
9
+ vi.mock('../services/configurationManager.js', () => ({
10
+ configurationManager: {
11
+ getStatusHooks: vi.fn(),
12
+ },
13
+ }));
14
+ // Mock the WorktreeService
15
+ vi.mock('../services/worktreeService.js', () => ({
16
+ WorktreeService: vi.fn(),
17
+ }));
18
+ // Note: This file contains integration tests that execute real commands
19
+ describe('hookExecutor Integration Tests', () => {
20
+ describe('executeHook (real execution)', () => {
21
+ it('should execute a simple echo command', async () => {
22
+ // Arrange
23
+ const tmpDir = await mkdtemp(join(tmpdir(), 'hook-test-'));
24
+ const environment = {
25
+ CCMANAGER_WORKTREE_PATH: tmpDir,
26
+ CCMANAGER_WORKTREE_BRANCH: 'test-branch',
27
+ CCMANAGER_GIT_ROOT: tmpDir,
28
+ };
29
+ try {
30
+ // Act & Assert - should not throw
31
+ await expect(executeHook('echo "Test successful"', tmpDir, environment)).resolves.toBeUndefined();
32
+ }
33
+ finally {
34
+ // Cleanup
35
+ await rm(tmpDir, { recursive: true });
36
+ }
37
+ });
38
+ it('should reject when command fails', async () => {
39
+ // Arrange
40
+ const tmpDir = await mkdtemp(join(tmpdir(), 'hook-test-'));
41
+ const environment = {
42
+ CCMANAGER_WORKTREE_PATH: tmpDir,
43
+ CCMANAGER_WORKTREE_BRANCH: 'test-branch',
44
+ CCMANAGER_GIT_ROOT: tmpDir,
45
+ };
46
+ try {
47
+ // Act & Assert
48
+ await expect(executeHook('exit 1', tmpDir, environment)).rejects.toThrow();
49
+ }
50
+ finally {
51
+ // Cleanup
52
+ await rm(tmpDir, { recursive: true });
53
+ }
54
+ });
55
+ it('should include stderr in error message when command fails', async () => {
56
+ // Arrange
57
+ const tmpDir = await mkdtemp(join(tmpdir(), 'hook-test-'));
58
+ const environment = {
59
+ CCMANAGER_WORKTREE_PATH: tmpDir,
60
+ CCMANAGER_WORKTREE_BRANCH: 'test-branch',
61
+ CCMANAGER_GIT_ROOT: tmpDir,
62
+ };
63
+ try {
64
+ // Act & Assert - command that writes to stderr and exits with error
65
+ await expect(executeHook('>&2 echo "Error details here"; exit 1', tmpDir, environment)).rejects.toThrow('Hook exited with code 1\nStderr: Error details here\n');
66
+ }
67
+ finally {
68
+ // Cleanup
69
+ await rm(tmpDir, { recursive: true });
70
+ }
71
+ });
72
+ it('should verify stderr handling in error messages', async () => {
73
+ // Arrange
74
+ const tmpDir = await mkdtemp(join(tmpdir(), 'hook-test-'));
75
+ const environment = {
76
+ CCMANAGER_WORKTREE_PATH: tmpDir,
77
+ CCMANAGER_WORKTREE_BRANCH: 'test-branch',
78
+ CCMANAGER_GIT_ROOT: tmpDir,
79
+ };
80
+ try {
81
+ // Test with multiline stderr
82
+ try {
83
+ await executeHook('>&2 echo "Line 1"; >&2 echo "Line 2"; exit 3', tmpDir, environment);
84
+ expect.fail('Should have thrown');
85
+ }
86
+ catch (error) {
87
+ expect(error).toBeInstanceOf(Error);
88
+ expect(error.message).toContain('Hook exited with code 3');
89
+ expect(error.message).toContain('Stderr: Line 1\nLine 2');
90
+ }
91
+ // Test with empty stderr
92
+ try {
93
+ await executeHook('exit 4', tmpDir, environment);
94
+ expect.fail('Should have thrown');
95
+ }
96
+ catch (error) {
97
+ expect(error).toBeInstanceOf(Error);
98
+ expect(error.message).toBe('Hook exited with code 4');
99
+ }
100
+ }
101
+ finally {
102
+ // Cleanup
103
+ await rm(tmpDir, { recursive: true });
104
+ }
105
+ });
106
+ it('should ignore stderr when command succeeds', async () => {
107
+ // Arrange
108
+ const tmpDir = await mkdtemp(join(tmpdir(), 'hook-test-'));
109
+ const environment = {
110
+ CCMANAGER_WORKTREE_PATH: tmpDir,
111
+ CCMANAGER_WORKTREE_BRANCH: 'test-branch',
112
+ CCMANAGER_GIT_ROOT: tmpDir,
113
+ };
114
+ try {
115
+ // Act - command that writes to stderr but exits successfully
116
+ // Should not throw even though there's stderr output
117
+ await expect(executeHook('>&2 echo "Warning message"; exit 0', tmpDir, environment)).resolves.toBeUndefined();
118
+ }
119
+ finally {
120
+ // Cleanup
121
+ await rm(tmpDir, { recursive: true });
122
+ }
123
+ });
124
+ it('should execute hook in the specified working directory', async () => {
125
+ // Arrange
126
+ const tmpDir = await mkdtemp(join(tmpdir(), 'hook-cwd-test-'));
127
+ const outputFile = join(tmpDir, 'cwd.txt');
128
+ const environment = {
129
+ CCMANAGER_WORKTREE_PATH: tmpDir,
130
+ CCMANAGER_WORKTREE_BRANCH: 'test-branch',
131
+ CCMANAGER_GIT_ROOT: '/some/other/path',
132
+ };
133
+ try {
134
+ // Act - write current directory to file
135
+ await executeHook(`pwd > "${outputFile}"`, tmpDir, environment);
136
+ // Read the output
137
+ const { readFile } = await import('fs/promises');
138
+ const output = await readFile(outputFile, 'utf-8');
139
+ // Assert - should be executed in tmpDir
140
+ expect(output.trim()).toBe(tmpDir);
141
+ }
142
+ finally {
143
+ // Cleanup
144
+ await rm(tmpDir, { recursive: true });
145
+ }
146
+ });
147
+ });
148
+ describe('executeWorktreePostCreationHook (real execution)', () => {
149
+ it('should not throw even when command fails', async () => {
150
+ // Arrange
151
+ const tmpDir = await mkdtemp(join(tmpdir(), 'hook-test-'));
152
+ const worktree = {
153
+ path: tmpDir,
154
+ branch: 'test-branch',
155
+ isMainWorktree: false,
156
+ hasSession: false,
157
+ };
158
+ try {
159
+ // Act & Assert - should not throw even with failing command
160
+ await expect(executeWorktreePostCreationHook('exit 1', worktree, tmpDir, 'main')).resolves.toBeUndefined();
161
+ }
162
+ finally {
163
+ // Cleanup
164
+ await rm(tmpDir, { recursive: true });
165
+ }
166
+ });
167
+ it('should execute worktree hook in the worktree path by default', async () => {
168
+ // Arrange
169
+ const tmpDir = await mkdtemp(join(tmpdir(), 'hook-worktree-test-'));
170
+ const outputFile = join(tmpDir, 'cwd.txt');
171
+ const worktree = {
172
+ path: tmpDir,
173
+ branch: 'test-branch',
174
+ isMainWorktree: false,
175
+ hasSession: false,
176
+ };
177
+ const gitRoot = '/different/git/root';
178
+ try {
179
+ // Act - write current directory to file
180
+ await executeWorktreePostCreationHook(`pwd > "${outputFile}"`, worktree, gitRoot, 'main');
181
+ // Read the output
182
+ const { readFile } = await import('fs/promises');
183
+ const output = await readFile(outputFile, 'utf-8');
184
+ // Assert - should be executed in worktree path, not git root
185
+ expect(output.trim()).toBe(tmpDir);
186
+ expect(output.trim()).not.toBe(gitRoot);
187
+ }
188
+ finally {
189
+ // Cleanup
190
+ await rm(tmpDir, { recursive: true });
191
+ }
192
+ });
193
+ it('should allow changing to git root using environment variable', async () => {
194
+ // Arrange
195
+ const tmpWorktreeDir = await mkdtemp(join(tmpdir(), 'hook-worktree-'));
196
+ const tmpGitRootDir = await mkdtemp(join(tmpdir(), 'hook-gitroot-'));
197
+ const outputFile = join(tmpWorktreeDir, 'gitroot.txt');
198
+ const worktree = {
199
+ path: tmpWorktreeDir,
200
+ branch: 'test-branch',
201
+ isMainWorktree: false,
202
+ hasSession: false,
203
+ };
204
+ try {
205
+ // Act - change to git root and write its path
206
+ await executeWorktreePostCreationHook(`cd "$CCMANAGER_GIT_ROOT" && pwd > "${outputFile}"`, worktree, tmpGitRootDir, 'main');
207
+ // Read the output
208
+ const { readFile } = await import('fs/promises');
209
+ const output = await readFile(outputFile, 'utf-8');
210
+ // Assert - should have changed to git root
211
+ expect(output.trim()).toBe(tmpGitRootDir);
212
+ }
213
+ finally {
214
+ // Cleanup
215
+ await rm(tmpWorktreeDir, { recursive: true });
216
+ await rm(tmpGitRootDir, { recursive: true });
217
+ }
218
+ });
219
+ it('should wait for all child processes to complete', async () => {
220
+ // Arrange
221
+ const tmpDir = await mkdtemp(join(tmpdir(), 'hook-wait-test-'));
222
+ const outputFile = join(tmpDir, 'delayed.txt');
223
+ const worktree = {
224
+ path: tmpDir,
225
+ branch: 'test-branch',
226
+ isMainWorktree: false,
227
+ hasSession: false,
228
+ };
229
+ try {
230
+ // Act - execute a command that spawns a background process with a delay
231
+ // The background process writes to a file after a delay
232
+ // We use a shell command that creates a background process and then exits
233
+ await executeWorktreePostCreationHook(`(sleep 0.1 && echo "completed" > "${outputFile}") & wait`, worktree, tmpDir, 'main');
234
+ // Read the output - this should exist because we waited for the background process
235
+ const { readFile } = await import('fs/promises');
236
+ const output = await readFile(outputFile, 'utf-8');
237
+ // Assert - the file should contain the expected content
238
+ expect(output.trim()).toBe('completed');
239
+ }
240
+ finally {
241
+ // Cleanup
242
+ await rm(tmpDir, { recursive: true });
243
+ }
244
+ });
245
+ });
246
+ describe('executeStatusHook', () => {
247
+ it('should wait for hook execution to complete', async () => {
248
+ // Arrange
249
+ const tmpDir = await mkdtemp(join(tmpdir(), 'status-hook-test-'));
250
+ const outputFile = join(tmpDir, 'status-hook-output.txt');
251
+ const mockSession = {
252
+ id: 'test-session-123',
253
+ worktreePath: tmpDir, // Use tmpDir as the worktree path
254
+ process: {},
255
+ terminal: {},
256
+ output: [],
257
+ outputHistory: [],
258
+ state: 'idle',
259
+ stateCheckInterval: undefined,
260
+ isPrimaryCommand: true,
261
+ commandConfig: undefined,
262
+ detectionStrategy: 'claude',
263
+ devcontainerConfig: undefined,
264
+ pendingState: undefined,
265
+ pendingStateStart: undefined,
266
+ lastActivity: new Date(),
267
+ isActive: true,
268
+ };
269
+ // Mock WorktreeService to return a worktree with the tmpDir path
270
+ vi.mocked(WorktreeService).mockImplementation(() => ({
271
+ getWorktrees: vi.fn(() => [
272
+ {
273
+ path: tmpDir,
274
+ branch: 'test-branch',
275
+ isMainWorktree: false,
276
+ hasSession: true,
277
+ },
278
+ ]),
279
+ }));
280
+ // Configure mock to return a hook that writes to a file with delay
281
+ vi.mocked(configurationManager.getStatusHooks).mockReturnValue({
282
+ busy: {
283
+ enabled: true,
284
+ command: `sleep 0.1 && echo "Hook executed" > "${outputFile}"`,
285
+ },
286
+ idle: { enabled: false, command: '' },
287
+ waiting_input: { enabled: false, command: '' },
288
+ });
289
+ try {
290
+ // Act - execute the hook and await it
291
+ await executeStatusHook('idle', 'busy', mockSession);
292
+ // Assert - file should exist because we awaited the hook
293
+ const content = await readFile(outputFile, 'utf-8');
294
+ expect(content.trim()).toBe('Hook executed');
295
+ }
296
+ finally {
297
+ // Cleanup
298
+ await rm(tmpDir, { recursive: true });
299
+ }
300
+ });
301
+ it('should handle hook execution errors gracefully', async () => {
302
+ // Arrange
303
+ const tmpDir = await mkdtemp(join(tmpdir(), 'status-hook-test-'));
304
+ const mockSession = {
305
+ id: 'test-session-456',
306
+ worktreePath: tmpDir, // Use tmpDir as the worktree path
307
+ process: {},
308
+ terminal: {},
309
+ output: [],
310
+ outputHistory: [],
311
+ state: 'idle',
312
+ stateCheckInterval: undefined,
313
+ isPrimaryCommand: true,
314
+ commandConfig: undefined,
315
+ detectionStrategy: 'claude',
316
+ devcontainerConfig: undefined,
317
+ pendingState: undefined,
318
+ pendingStateStart: undefined,
319
+ lastActivity: new Date(),
320
+ isActive: true,
321
+ };
322
+ // Mock WorktreeService to return a worktree with the tmpDir path
323
+ vi.mocked(WorktreeService).mockImplementation(() => ({
324
+ getWorktrees: vi.fn(() => [
325
+ {
326
+ path: tmpDir,
327
+ branch: 'test-branch',
328
+ isMainWorktree: false,
329
+ hasSession: true,
330
+ },
331
+ ]),
332
+ }));
333
+ // Configure mock to return a hook that fails
334
+ vi.mocked(configurationManager.getStatusHooks).mockReturnValue({
335
+ busy: {
336
+ enabled: true,
337
+ command: 'exit 1',
338
+ },
339
+ idle: { enabled: false, command: '' },
340
+ waiting_input: { enabled: false, command: '' },
341
+ });
342
+ try {
343
+ // Act & Assert - should not throw even when hook fails
344
+ await expect(executeStatusHook('idle', 'busy', mockSession)).resolves.toBeUndefined();
345
+ }
346
+ finally {
347
+ // Cleanup
348
+ await rm(tmpDir, { recursive: true });
349
+ }
350
+ });
351
+ it('should not execute disabled hooks', async () => {
352
+ // Arrange
353
+ const tmpDir = await mkdtemp(join(tmpdir(), 'status-hook-test-'));
354
+ const outputFile = join(tmpDir, 'should-not-exist.txt');
355
+ const mockSession = {
356
+ id: 'test-session-789',
357
+ worktreePath: tmpDir, // Use tmpDir as the worktree path
358
+ process: {},
359
+ terminal: {},
360
+ output: [],
361
+ outputHistory: [],
362
+ state: 'idle',
363
+ stateCheckInterval: undefined,
364
+ isPrimaryCommand: true,
365
+ commandConfig: undefined,
366
+ detectionStrategy: 'claude',
367
+ devcontainerConfig: undefined,
368
+ pendingState: undefined,
369
+ pendingStateStart: undefined,
370
+ lastActivity: new Date(),
371
+ isActive: true,
372
+ };
373
+ // Mock WorktreeService to return a worktree with the tmpDir path
374
+ vi.mocked(WorktreeService).mockImplementation(() => ({
375
+ getWorktrees: vi.fn(() => [
376
+ {
377
+ path: tmpDir,
378
+ branch: 'test-branch',
379
+ isMainWorktree: false,
380
+ hasSession: true,
381
+ },
382
+ ]),
383
+ }));
384
+ // Configure mock to return a disabled hook
385
+ vi.mocked(configurationManager.getStatusHooks).mockReturnValue({
386
+ busy: {
387
+ enabled: false,
388
+ command: `echo "Should not run" > "${outputFile}"`,
389
+ },
390
+ idle: { enabled: false, command: '' },
391
+ waiting_input: { enabled: false, command: '' },
392
+ });
393
+ try {
394
+ // Act
395
+ await executeStatusHook('idle', 'busy', mockSession);
396
+ // Assert - file should not exist because hook was disabled
397
+ await expect(readFile(outputFile, 'utf-8')).rejects.toThrow();
398
+ }
399
+ finally {
400
+ // Cleanup
401
+ await rm(tmpDir, { recursive: true });
402
+ }
403
+ });
404
+ });
405
+ });
@@ -105,6 +105,13 @@ describe('prepareWorktreeItems', () => {
105
105
  lastActivity: new Date(),
106
106
  isActive: true,
107
107
  terminal: {},
108
+ stateCheckInterval: undefined,
109
+ isPrimaryCommand: true,
110
+ commandConfig: undefined,
111
+ detectionStrategy: 'claude',
112
+ devcontainerConfig: undefined,
113
+ pendingState: undefined,
114
+ pendingStateStart: undefined,
108
115
  };
109
116
  it('should prepare basic worktree without git status', () => {
110
117
  const items = prepareWorktreeItems([mockWorktree], []);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",
@@ -1,6 +0,0 @@
1
- import React from 'react';
2
- interface ConfigureHooksProps {
3
- onComplete: () => void;
4
- }
5
- declare const ConfigureHooks: React.FC<ConfigureHooksProps>;
6
- export default ConfigureHooks;