ccmanager 2.0.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 +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/components/Menu.js +3 -2
- package/dist/components/NewWorktree.js +30 -2
- 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 +3 -33
- 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 +10 -2
- 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 +387 -0
- package/package.json +1 -1
- package/dist/components/ConfigureHooks.d.ts +0 -6
|
@@ -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 {
|
|
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
|
-
|
|
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) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
2
|
import { WorktreeService } from './worktreeService.js';
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
4
|
import { existsSync, statSync } from 'fs';
|
|
5
|
+
import { configurationManager } from './configurationManager.js';
|
|
6
|
+
import { executeWorktreePostCreationHook } from '../utils/hookExecutor.js';
|
|
5
7
|
// Mock child_process module
|
|
6
8
|
vi.mock('child_process');
|
|
7
9
|
// Mock fs module
|
|
@@ -14,10 +16,22 @@ vi.mock('./worktreeConfigManager.js', () => ({
|
|
|
14
16
|
reset: vi.fn(),
|
|
15
17
|
},
|
|
16
18
|
}));
|
|
19
|
+
// Mock configurationManager
|
|
20
|
+
vi.mock('./configurationManager.js', () => ({
|
|
21
|
+
configurationManager: {
|
|
22
|
+
getWorktreeHooks: vi.fn(),
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
// Mock HookExecutor
|
|
26
|
+
vi.mock('../utils/hookExecutor.js', () => ({
|
|
27
|
+
executeWorktreePostCreationHook: vi.fn(),
|
|
28
|
+
}));
|
|
17
29
|
// Get the mocked function with proper typing
|
|
18
30
|
const mockedExecSync = vi.mocked(execSync);
|
|
19
31
|
const mockedExistsSync = vi.mocked(existsSync);
|
|
20
32
|
const mockedStatSync = vi.mocked(statSync);
|
|
33
|
+
const mockedGetWorktreeHooks = vi.mocked(configurationManager.getWorktreeHooks);
|
|
34
|
+
const mockedExecuteHook = vi.mocked(executeWorktreePostCreationHook);
|
|
21
35
|
describe('WorktreeService', () => {
|
|
22
36
|
let service;
|
|
23
37
|
beforeEach(() => {
|
|
@@ -29,6 +43,8 @@ describe('WorktreeService', () => {
|
|
|
29
43
|
}
|
|
30
44
|
throw new Error('Command not mocked: ' + cmd);
|
|
31
45
|
});
|
|
46
|
+
// Default mock for getWorktreeHooks to return empty config
|
|
47
|
+
mockedGetWorktreeHooks.mockReturnValue({});
|
|
32
48
|
service = new WorktreeService('/fake/path');
|
|
33
49
|
});
|
|
34
50
|
describe('getGitRootPath', () => {
|
|
@@ -177,7 +193,7 @@ origin/feature/test
|
|
|
177
193
|
});
|
|
178
194
|
});
|
|
179
195
|
describe('createWorktree', () => {
|
|
180
|
-
it('should create worktree with base branch when branch does not exist', () => {
|
|
196
|
+
it('should create worktree with base branch when branch does not exist', async () => {
|
|
181
197
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
182
198
|
if (typeof cmd === 'string') {
|
|
183
199
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
@@ -190,11 +206,11 @@ origin/feature/test
|
|
|
190
206
|
}
|
|
191
207
|
throw new Error('Unexpected command');
|
|
192
208
|
});
|
|
193
|
-
const result = service.createWorktree('/path/to/worktree', 'new-feature', 'develop');
|
|
209
|
+
const result = await service.createWorktree('/path/to/worktree', 'new-feature', 'develop');
|
|
194
210
|
expect(result).toEqual({ success: true });
|
|
195
211
|
expect(execSync).toHaveBeenCalledWith('git worktree add -b "new-feature" "/path/to/worktree" "develop"', expect.any(Object));
|
|
196
212
|
});
|
|
197
|
-
it('should create worktree without base branch when branch exists', () => {
|
|
213
|
+
it('should create worktree without base branch when branch exists', async () => {
|
|
198
214
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
199
215
|
if (typeof cmd === 'string') {
|
|
200
216
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
@@ -207,11 +223,11 @@ origin/feature/test
|
|
|
207
223
|
}
|
|
208
224
|
throw new Error('Unexpected command');
|
|
209
225
|
});
|
|
210
|
-
const result = service.createWorktree('/path/to/worktree', 'existing-feature', 'main');
|
|
226
|
+
const result = await service.createWorktree('/path/to/worktree', 'existing-feature', 'main');
|
|
211
227
|
expect(result).toEqual({ success: true });
|
|
212
228
|
expect(execSync).toHaveBeenCalledWith('git worktree add "/path/to/worktree" "existing-feature"', expect.any(Object));
|
|
213
229
|
});
|
|
214
|
-
it('should create worktree from specified base branch when branch does not exist', () => {
|
|
230
|
+
it('should create worktree from specified base branch when branch does not exist', async () => {
|
|
215
231
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
216
232
|
if (typeof cmd === 'string') {
|
|
217
233
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
@@ -224,7 +240,7 @@ origin/feature/test
|
|
|
224
240
|
}
|
|
225
241
|
throw new Error('Unexpected command');
|
|
226
242
|
});
|
|
227
|
-
const result = service.createWorktree('/path/to/worktree', 'new-feature', 'main');
|
|
243
|
+
const result = await service.createWorktree('/path/to/worktree', 'new-feature', 'main');
|
|
228
244
|
expect(result).toEqual({ success: true });
|
|
229
245
|
expect(execSync).toHaveBeenCalledWith('git worktree add -b "new-feature" "/path/to/worktree" "main"', expect.any(Object));
|
|
230
246
|
});
|
|
@@ -389,4 +405,143 @@ branch refs/heads/other-branch
|
|
|
389
405
|
expect(existsSync).toHaveBeenCalledWith('/fake/path/.claude');
|
|
390
406
|
});
|
|
391
407
|
});
|
|
408
|
+
describe('Worktree Hook Execution', () => {
|
|
409
|
+
afterEach(() => {
|
|
410
|
+
vi.clearAllMocks();
|
|
411
|
+
});
|
|
412
|
+
it('should execute post-creation hook when worktree is created', async () => {
|
|
413
|
+
// Arrange
|
|
414
|
+
const hookCommand = 'echo "Worktree created: $CCMANAGER_WORKTREE_PATH"';
|
|
415
|
+
mockedGetWorktreeHooks.mockReturnValue({
|
|
416
|
+
post_creation: {
|
|
417
|
+
command: hookCommand,
|
|
418
|
+
enabled: true,
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
mockedExecuteHook.mockResolvedValue(undefined);
|
|
422
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
423
|
+
if (typeof cmd === 'string') {
|
|
424
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
425
|
+
return '/fake/path/.git\n';
|
|
426
|
+
}
|
|
427
|
+
if (cmd.includes('git worktree list')) {
|
|
428
|
+
return 'worktree /fake/path\nHEAD abc123\nbranch refs/heads/main\n';
|
|
429
|
+
}
|
|
430
|
+
if (cmd.includes('git worktree add')) {
|
|
431
|
+
return '';
|
|
432
|
+
}
|
|
433
|
+
if (cmd.includes('git rev-parse --verify')) {
|
|
434
|
+
throw new Error('Branch not found');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return '';
|
|
438
|
+
});
|
|
439
|
+
// Act
|
|
440
|
+
const result = await service.createWorktree('feature-branch-dir', 'feature-branch', 'main', false, false);
|
|
441
|
+
// Assert
|
|
442
|
+
expect(result.success).toBe(true);
|
|
443
|
+
expect(mockedGetWorktreeHooks).toHaveBeenCalled();
|
|
444
|
+
expect(mockedExecuteHook).toHaveBeenCalledWith(hookCommand, expect.objectContaining({
|
|
445
|
+
path: '/fake/path/feature-branch-dir',
|
|
446
|
+
branch: 'feature-branch',
|
|
447
|
+
isMainWorktree: false,
|
|
448
|
+
hasSession: false,
|
|
449
|
+
}), '/fake/path', 'main');
|
|
450
|
+
});
|
|
451
|
+
it('should not execute hook when disabled', async () => {
|
|
452
|
+
// Arrange
|
|
453
|
+
mockedGetWorktreeHooks.mockReturnValue({
|
|
454
|
+
post_creation: {
|
|
455
|
+
command: 'echo "Should not run"',
|
|
456
|
+
enabled: false,
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
460
|
+
if (typeof cmd === 'string') {
|
|
461
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
462
|
+
return '/fake/path/.git\n';
|
|
463
|
+
}
|
|
464
|
+
if (cmd.includes('git worktree list')) {
|
|
465
|
+
return 'worktree /fake/path\nHEAD abc123\nbranch refs/heads/main\n';
|
|
466
|
+
}
|
|
467
|
+
if (cmd.includes('git worktree add')) {
|
|
468
|
+
return '';
|
|
469
|
+
}
|
|
470
|
+
if (cmd.includes('git rev-parse --verify')) {
|
|
471
|
+
throw new Error('Branch not found');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return '';
|
|
475
|
+
});
|
|
476
|
+
// Act
|
|
477
|
+
const result = await service.createWorktree('feature-branch-dir', 'feature-branch', 'main', false, false);
|
|
478
|
+
// Assert
|
|
479
|
+
expect(result.success).toBe(true);
|
|
480
|
+
expect(mockedGetWorktreeHooks).toHaveBeenCalled();
|
|
481
|
+
expect(mockedExecuteHook).not.toHaveBeenCalled();
|
|
482
|
+
});
|
|
483
|
+
it('should not execute hook when not configured', async () => {
|
|
484
|
+
// Arrange
|
|
485
|
+
mockedGetWorktreeHooks.mockReturnValue({});
|
|
486
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
487
|
+
if (typeof cmd === 'string') {
|
|
488
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
489
|
+
return '/fake/path/.git\n';
|
|
490
|
+
}
|
|
491
|
+
if (cmd.includes('git worktree list')) {
|
|
492
|
+
return 'worktree /fake/path\nHEAD abc123\nbranch refs/heads/main\n';
|
|
493
|
+
}
|
|
494
|
+
if (cmd.includes('git worktree add')) {
|
|
495
|
+
return '';
|
|
496
|
+
}
|
|
497
|
+
if (cmd.includes('git rev-parse --verify')) {
|
|
498
|
+
throw new Error('Branch not found');
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return '';
|
|
502
|
+
});
|
|
503
|
+
// Act
|
|
504
|
+
const result = await service.createWorktree('feature-branch-dir', 'feature-branch', 'main', false, false);
|
|
505
|
+
// Assert
|
|
506
|
+
expect(result.success).toBe(true);
|
|
507
|
+
expect(mockedGetWorktreeHooks).toHaveBeenCalled();
|
|
508
|
+
expect(mockedExecuteHook).not.toHaveBeenCalled();
|
|
509
|
+
});
|
|
510
|
+
it('should not fail worktree creation if hook execution fails', async () => {
|
|
511
|
+
// Arrange
|
|
512
|
+
mockedGetWorktreeHooks.mockReturnValue({
|
|
513
|
+
post_creation: {
|
|
514
|
+
command: 'failing-command',
|
|
515
|
+
enabled: true,
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
// The real executeWorktreePostCreationHook doesn't throw, it catches errors internally
|
|
519
|
+
// So the mock should resolve, not reject
|
|
520
|
+
mockedExecuteHook.mockResolvedValue(undefined);
|
|
521
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
522
|
+
if (typeof cmd === 'string') {
|
|
523
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
524
|
+
return '/fake/path/.git\n';
|
|
525
|
+
}
|
|
526
|
+
if (cmd.includes('git worktree list')) {
|
|
527
|
+
return 'worktree /fake/path\nHEAD abc123\nbranch refs/heads/main\n';
|
|
528
|
+
}
|
|
529
|
+
if (cmd.includes('git worktree add')) {
|
|
530
|
+
return '';
|
|
531
|
+
}
|
|
532
|
+
if (cmd.includes('git rev-parse --verify')) {
|
|
533
|
+
throw new Error('Branch not found');
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return '';
|
|
537
|
+
});
|
|
538
|
+
// Act
|
|
539
|
+
const result = await service.createWorktree('feature-branch-dir', 'feature-branch', 'main', false, false);
|
|
540
|
+
// Allow async operations to complete
|
|
541
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
542
|
+
// Assert
|
|
543
|
+
expect(result.success).toBe(true);
|
|
544
|
+
expect(mockedExecuteHook).toHaveBeenCalled();
|
|
545
|
+
});
|
|
546
|
+
});
|
|
392
547
|
});
|
package/dist/types/index.d.ts
CHANGED
|
@@ -54,6 +54,13 @@ export interface StatusHookConfig {
|
|
|
54
54
|
busy?: StatusHook;
|
|
55
55
|
waiting_input?: StatusHook;
|
|
56
56
|
}
|
|
57
|
+
export interface WorktreeHook {
|
|
58
|
+
command: string;
|
|
59
|
+
enabled: boolean;
|
|
60
|
+
}
|
|
61
|
+
export interface WorktreeHookConfig {
|
|
62
|
+
post_creation?: WorktreeHook;
|
|
63
|
+
}
|
|
57
64
|
export interface WorktreeConfig {
|
|
58
65
|
autoDirectory: boolean;
|
|
59
66
|
autoDirectoryPattern?: string;
|
|
@@ -84,6 +91,7 @@ export interface DevcontainerConfig {
|
|
|
84
91
|
export interface ConfigurationData {
|
|
85
92
|
shortcuts?: ShortcutConfig;
|
|
86
93
|
statusHooks?: StatusHookConfig;
|
|
94
|
+
worktreeHooks?: WorktreeHookConfig;
|
|
87
95
|
worktree?: WorktreeConfig;
|
|
88
96
|
command?: CommandConfig;
|
|
89
97
|
commandPresets?: CommandPresetsConfig;
|
|
@@ -126,10 +134,10 @@ export interface IProjectManager {
|
|
|
126
134
|
export interface IWorktreeService {
|
|
127
135
|
getWorktrees(): Worktree[];
|
|
128
136
|
getGitRootPath(): string;
|
|
129
|
-
createWorktree(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean): {
|
|
137
|
+
createWorktree(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean): Promise<{
|
|
130
138
|
success: boolean;
|
|
131
139
|
error?: string;
|
|
132
|
-
}
|
|
140
|
+
}>;
|
|
133
141
|
deleteWorktree(worktreePath: string, options?: {
|
|
134
142
|
deleteBranch?: boolean;
|
|
135
143
|
}): {
|
|
@@ -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 {};
|