ccmanager 3.1.4 → 3.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/bin/cli.js +85 -0
- package/dist/cli.test.js +9 -10
- package/dist/components/App.test.js +6 -4
- package/dist/components/DeleteWorktree.test.js +22 -10
- package/dist/components/Menu.recent-projects.test.js +22 -0
- package/dist/components/Menu.test.js +5 -3
- package/dist/components/MergeWorktree.test.js +12 -4
- package/dist/components/NewWorktree.test.js +57 -39
- package/dist/components/ProjectList.recent-projects.test.js +5 -3
- package/dist/components/ProjectList.test.js +25 -3
- package/dist/components/Session.js +6 -5
- package/dist/services/bunTerminal.d.ts +53 -0
- package/dist/services/bunTerminal.js +175 -0
- package/dist/services/sessionManager.autoApproval.test.js +73 -41
- package/dist/services/sessionManager.effect.test.js +17 -13
- package/dist/services/sessionManager.js +122 -76
- package/dist/services/sessionManager.statePersistence.test.js +64 -62
- package/dist/services/sessionManager.test.js +44 -23
- package/dist/types/index.d.ts +8 -7
- package/dist/utils/hookExecutor.test.js +55 -54
- package/dist/utils/mutex.d.ts +54 -0
- package/dist/utils/mutex.js +104 -0
- package/dist/utils/worktreeUtils.js +3 -1
- package/dist/utils/worktreeUtils.test.js +5 -4
- package/package.json +39 -29
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
2
|
import { Effect } from 'effect';
|
|
3
|
-
import { spawn } from '
|
|
3
|
+
import { spawn } from './bunTerminal.js';
|
|
4
4
|
import { EventEmitter } from 'events';
|
|
5
5
|
import { exec } from 'child_process';
|
|
6
|
-
// Mock
|
|
7
|
-
vi.mock('
|
|
8
|
-
spawn: vi.fn()
|
|
6
|
+
// Mock bunTerminal
|
|
7
|
+
vi.mock('./bunTerminal.js', () => ({
|
|
8
|
+
spawn: vi.fn(function () {
|
|
9
|
+
return null;
|
|
10
|
+
}),
|
|
9
11
|
}));
|
|
10
12
|
// Mock child_process
|
|
11
13
|
vi.mock('child_process', () => ({
|
|
12
|
-
exec: vi.fn()
|
|
13
|
-
|
|
14
|
+
exec: vi.fn(function () {
|
|
15
|
+
return null;
|
|
16
|
+
}),
|
|
17
|
+
execFile: vi.fn(function () {
|
|
18
|
+
return null;
|
|
19
|
+
}),
|
|
14
20
|
}));
|
|
15
21
|
// Mock configuration manager
|
|
16
22
|
vi.mock('./configurationManager.js', () => ({
|
|
@@ -29,20 +35,28 @@ vi.mock('./configurationManager.js', () => ({
|
|
|
29
35
|
// Mock Terminal
|
|
30
36
|
vi.mock('@xterm/headless', () => ({
|
|
31
37
|
default: {
|
|
32
|
-
Terminal: vi.fn(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
Terminal: vi.fn(function () {
|
|
39
|
+
return {
|
|
40
|
+
buffer: {
|
|
41
|
+
active: {
|
|
42
|
+
length: 0,
|
|
43
|
+
getLine: vi.fn(function () {
|
|
44
|
+
return null;
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
37
47
|
},
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
write: vi.fn(function () {
|
|
49
|
+
return undefined;
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
}),
|
|
41
53
|
},
|
|
42
54
|
}));
|
|
43
55
|
// Mock worktreeService
|
|
44
56
|
vi.mock('./worktreeService.js', () => ({
|
|
45
|
-
WorktreeService: vi.fn()
|
|
57
|
+
WorktreeService: vi.fn(function () {
|
|
58
|
+
return {};
|
|
59
|
+
}),
|
|
46
60
|
}));
|
|
47
61
|
// Create a mock IPty class
|
|
48
62
|
class MockPty extends EventEmitter {
|
|
@@ -705,13 +719,20 @@ describe('SessionManager', () => {
|
|
|
705
719
|
});
|
|
706
720
|
describe('static methods', () => {
|
|
707
721
|
describe('getSessionCounts', () => {
|
|
722
|
+
// Helper to create mock session with stateMutex
|
|
723
|
+
const createMockSession = (id, state) => ({
|
|
724
|
+
id,
|
|
725
|
+
stateMutex: {
|
|
726
|
+
getSnapshot: () => ({ state }),
|
|
727
|
+
},
|
|
728
|
+
});
|
|
708
729
|
it('should count sessions by state', () => {
|
|
709
730
|
const sessions = [
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
731
|
+
createMockSession('1', 'idle'),
|
|
732
|
+
createMockSession('2', 'busy'),
|
|
733
|
+
createMockSession('3', 'busy'),
|
|
734
|
+
createMockSession('4', 'waiting_input'),
|
|
735
|
+
createMockSession('5', 'idle'),
|
|
715
736
|
];
|
|
716
737
|
const counts = SessionManager.getSessionCounts(sessions);
|
|
717
738
|
expect(counts.idle).toBe(2);
|
|
@@ -728,9 +749,9 @@ describe('SessionManager', () => {
|
|
|
728
749
|
});
|
|
729
750
|
it('should handle sessions with single state', () => {
|
|
730
751
|
const sessions = [
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
752
|
+
createMockSession('1', 'busy'),
|
|
753
|
+
createMockSession('2', 'busy'),
|
|
754
|
+
createMockSession('3', 'busy'),
|
|
734
755
|
];
|
|
735
756
|
const counts = SessionManager.getSessionCounts(sessions);
|
|
736
757
|
expect(counts.idle).toBe(0);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { IPty } from '
|
|
1
|
+
import type { IPty } from '../services/bunTerminal.js';
|
|
2
2
|
import type pkg from '@xterm/headless';
|
|
3
3
|
import { GitStatus } from '../utils/gitStatus.js';
|
|
4
|
+
import { Mutex, SessionStateData } from '../utils/mutex.js';
|
|
4
5
|
export type Terminal = InstanceType<typeof pkg.Terminal>;
|
|
5
6
|
export type SessionState = 'idle' | 'busy' | 'waiting_input' | 'pending_auto_approval';
|
|
6
7
|
export type StateDetectionStrategy = 'claude' | 'gemini' | 'codex' | 'cursor' | 'github-copilot' | 'cline';
|
|
@@ -16,7 +17,6 @@ export interface Session {
|
|
|
16
17
|
id: string;
|
|
17
18
|
worktreePath: string;
|
|
18
19
|
process: IPty;
|
|
19
|
-
state: SessionState;
|
|
20
20
|
output: string[];
|
|
21
21
|
outputHistory: Buffer[];
|
|
22
22
|
lastActivity: Date;
|
|
@@ -27,11 +27,12 @@ export interface Session {
|
|
|
27
27
|
commandConfig: CommandConfig | undefined;
|
|
28
28
|
detectionStrategy: StateDetectionStrategy | undefined;
|
|
29
29
|
devcontainerConfig: DevcontainerConfig | undefined;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Mutex-protected session state data.
|
|
32
|
+
* Access via stateMutex.runExclusive() or stateMutex.update() to ensure thread-safe operations.
|
|
33
|
+
* Contains: state, pendingState, pendingStateStart, autoApprovalFailed, autoApprovalReason, autoApprovalAbortController
|
|
34
|
+
*/
|
|
35
|
+
stateMutex: Mutex<SessionStateData>;
|
|
35
36
|
}
|
|
36
37
|
export interface AutoApprovalResponse {
|
|
37
38
|
needsPermission: boolean;
|
|
@@ -7,6 +7,7 @@ import { join } from 'path';
|
|
|
7
7
|
import { configurationManager } from '../services/configurationManager.js';
|
|
8
8
|
import { WorktreeService } from '../services/worktreeService.js';
|
|
9
9
|
import { GitError } from '../types/errors.js';
|
|
10
|
+
import { Mutex, createInitialSessionStateData } from './mutex.js';
|
|
10
11
|
// Mock the configurationManager
|
|
11
12
|
vi.mock('../services/configurationManager.js', () => ({
|
|
12
13
|
configurationManager: {
|
|
@@ -15,7 +16,11 @@ vi.mock('../services/configurationManager.js', () => ({
|
|
|
15
16
|
}));
|
|
16
17
|
// Mock the WorktreeService
|
|
17
18
|
vi.mock('../services/worktreeService.js', () => ({
|
|
18
|
-
WorktreeService: vi.fn()
|
|
19
|
+
WorktreeService: vi.fn(function () {
|
|
20
|
+
return {
|
|
21
|
+
getWorktreesEffect: vi.fn(),
|
|
22
|
+
};
|
|
23
|
+
}),
|
|
19
24
|
}));
|
|
20
25
|
// Note: This file contains integration tests that execute real commands
|
|
21
26
|
describe('hookExecutor Integration Tests', () => {
|
|
@@ -262,29 +267,28 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
262
267
|
terminal: {},
|
|
263
268
|
output: [],
|
|
264
269
|
outputHistory: [],
|
|
265
|
-
state: 'idle',
|
|
266
270
|
stateCheckInterval: undefined,
|
|
267
271
|
isPrimaryCommand: true,
|
|
268
272
|
commandConfig: undefined,
|
|
269
273
|
detectionStrategy: 'claude',
|
|
270
274
|
devcontainerConfig: undefined,
|
|
271
|
-
pendingState: undefined,
|
|
272
|
-
pendingStateStart: undefined,
|
|
273
275
|
lastActivity: new Date(),
|
|
274
276
|
isActive: true,
|
|
275
|
-
|
|
277
|
+
stateMutex: new Mutex(createInitialSessionStateData()),
|
|
276
278
|
};
|
|
277
279
|
// Mock WorktreeService to return a worktree with the tmpDir path
|
|
278
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
280
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
281
|
+
return {
|
|
282
|
+
getWorktreesEffect: vi.fn(() => Effect.succeed([
|
|
283
|
+
{
|
|
284
|
+
path: tmpDir,
|
|
285
|
+
branch: 'test-branch',
|
|
286
|
+
isMainWorktree: false,
|
|
287
|
+
hasSession: true,
|
|
288
|
+
},
|
|
289
|
+
])),
|
|
290
|
+
};
|
|
291
|
+
});
|
|
288
292
|
// Configure mock to return a hook that writes to a file with delay
|
|
289
293
|
vi.mocked(configurationManager.getStatusHooks).mockReturnValue({
|
|
290
294
|
busy: {
|
|
@@ -317,29 +321,28 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
317
321
|
terminal: {},
|
|
318
322
|
output: [],
|
|
319
323
|
outputHistory: [],
|
|
320
|
-
state: 'idle',
|
|
321
324
|
stateCheckInterval: undefined,
|
|
322
325
|
isPrimaryCommand: true,
|
|
323
326
|
commandConfig: undefined,
|
|
324
327
|
detectionStrategy: 'claude',
|
|
325
328
|
devcontainerConfig: undefined,
|
|
326
|
-
pendingState: undefined,
|
|
327
|
-
pendingStateStart: undefined,
|
|
328
329
|
lastActivity: new Date(),
|
|
329
330
|
isActive: true,
|
|
330
|
-
|
|
331
|
+
stateMutex: new Mutex(createInitialSessionStateData()),
|
|
331
332
|
};
|
|
332
333
|
// Mock WorktreeService to return a worktree with the tmpDir path
|
|
333
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
334
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
335
|
+
return {
|
|
336
|
+
getWorktreesEffect: vi.fn(() => Effect.succeed([
|
|
337
|
+
{
|
|
338
|
+
path: tmpDir,
|
|
339
|
+
branch: 'test-branch',
|
|
340
|
+
isMainWorktree: false,
|
|
341
|
+
hasSession: true,
|
|
342
|
+
},
|
|
343
|
+
])),
|
|
344
|
+
};
|
|
345
|
+
});
|
|
343
346
|
// Configure mock to return a hook that fails
|
|
344
347
|
vi.mocked(configurationManager.getStatusHooks).mockReturnValue({
|
|
345
348
|
busy: {
|
|
@@ -370,29 +373,28 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
370
373
|
terminal: {},
|
|
371
374
|
output: [],
|
|
372
375
|
outputHistory: [],
|
|
373
|
-
state: 'idle',
|
|
374
376
|
stateCheckInterval: undefined,
|
|
375
377
|
isPrimaryCommand: true,
|
|
376
378
|
commandConfig: undefined,
|
|
377
379
|
detectionStrategy: 'claude',
|
|
378
380
|
devcontainerConfig: undefined,
|
|
379
|
-
pendingState: undefined,
|
|
380
|
-
pendingStateStart: undefined,
|
|
381
381
|
lastActivity: new Date(),
|
|
382
382
|
isActive: true,
|
|
383
|
-
|
|
383
|
+
stateMutex: new Mutex(createInitialSessionStateData()),
|
|
384
384
|
};
|
|
385
385
|
// Mock WorktreeService to return a worktree with the tmpDir path
|
|
386
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
386
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
387
|
+
return {
|
|
388
|
+
getWorktreesEffect: vi.fn(() => Effect.succeed([
|
|
389
|
+
{
|
|
390
|
+
path: tmpDir,
|
|
391
|
+
branch: 'test-branch',
|
|
392
|
+
isMainWorktree: false,
|
|
393
|
+
hasSession: true,
|
|
394
|
+
},
|
|
395
|
+
])),
|
|
396
|
+
};
|
|
397
|
+
});
|
|
396
398
|
// Configure mock to return a disabled hook
|
|
397
399
|
vi.mocked(configurationManager.getStatusHooks).mockReturnValue({
|
|
398
400
|
busy: {
|
|
@@ -425,26 +427,25 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
425
427
|
terminal: {},
|
|
426
428
|
output: [],
|
|
427
429
|
outputHistory: [],
|
|
428
|
-
state: 'idle',
|
|
429
430
|
stateCheckInterval: undefined,
|
|
430
431
|
isPrimaryCommand: true,
|
|
431
432
|
commandConfig: undefined,
|
|
432
433
|
detectionStrategy: 'claude',
|
|
433
434
|
devcontainerConfig: undefined,
|
|
434
|
-
pendingState: undefined,
|
|
435
|
-
pendingStateStart: undefined,
|
|
436
435
|
lastActivity: new Date(),
|
|
437
436
|
isActive: true,
|
|
438
|
-
|
|
437
|
+
stateMutex: new Mutex(createInitialSessionStateData()),
|
|
439
438
|
};
|
|
440
439
|
// Mock WorktreeService to fail with GitError
|
|
441
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
440
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
441
|
+
return {
|
|
442
|
+
getWorktreesEffect: vi.fn(() => Effect.fail(new GitError({
|
|
443
|
+
command: 'git worktree list --porcelain',
|
|
444
|
+
exitCode: 128,
|
|
445
|
+
stderr: 'not a git repository',
|
|
446
|
+
}))),
|
|
447
|
+
};
|
|
448
|
+
});
|
|
448
449
|
// Configure mock to return a hook that should execute despite worktree query failure
|
|
449
450
|
vi.mocked(configurationManager.getStatusHooks).mockReturnValue({
|
|
450
451
|
busy: {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A simple mutex implementation for protecting shared state.
|
|
3
|
+
* Provides exclusive access to wrapped data through async locking.
|
|
4
|
+
*/
|
|
5
|
+
export declare class Mutex<T> {
|
|
6
|
+
private data;
|
|
7
|
+
private locked;
|
|
8
|
+
private waitQueue;
|
|
9
|
+
constructor(initialData: T);
|
|
10
|
+
/**
|
|
11
|
+
* Acquire the lock. Returns a promise that resolves when the lock is acquired.
|
|
12
|
+
*/
|
|
13
|
+
private acquire;
|
|
14
|
+
/**
|
|
15
|
+
* Release the lock, allowing the next waiter to proceed.
|
|
16
|
+
*/
|
|
17
|
+
private release;
|
|
18
|
+
/**
|
|
19
|
+
* Run a function with exclusive access to the protected data.
|
|
20
|
+
* The lock is acquired before the function runs and released after it completes.
|
|
21
|
+
*
|
|
22
|
+
* @param fn - Function that receives the current data and returns updated data or a promise of updated data
|
|
23
|
+
* @returns Promise that resolves with the function's return value
|
|
24
|
+
*/
|
|
25
|
+
runExclusive<R>(fn: (data: T) => R | Promise<R>): Promise<R>;
|
|
26
|
+
/**
|
|
27
|
+
* Run a function with exclusive access and update the protected data.
|
|
28
|
+
* The lock is acquired before the function runs and released after it completes.
|
|
29
|
+
*
|
|
30
|
+
* @param fn - Function that receives the current data and returns the updated data
|
|
31
|
+
*/
|
|
32
|
+
update(fn: (data: T) => T | Promise<T>): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Get a snapshot of the current data without acquiring the lock.
|
|
35
|
+
* Use with caution - this does not guarantee consistency.
|
|
36
|
+
* Prefer runExclusive for reads that need to be consistent with writes.
|
|
37
|
+
*/
|
|
38
|
+
getSnapshot(): T;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Interface for the session state data protected by mutex.
|
|
42
|
+
*/
|
|
43
|
+
export interface SessionStateData {
|
|
44
|
+
state: import('../types/index.js').SessionState;
|
|
45
|
+
pendingState: import('../types/index.js').SessionState | undefined;
|
|
46
|
+
pendingStateStart: number | undefined;
|
|
47
|
+
autoApprovalFailed: boolean;
|
|
48
|
+
autoApprovalReason: string | undefined;
|
|
49
|
+
autoApprovalAbortController: AbortController | undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Create initial session state data with default values.
|
|
53
|
+
*/
|
|
54
|
+
export declare function createInitialSessionStateData(): SessionStateData;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A simple mutex implementation for protecting shared state.
|
|
3
|
+
* Provides exclusive access to wrapped data through async locking.
|
|
4
|
+
*/
|
|
5
|
+
export class Mutex {
|
|
6
|
+
constructor(initialData) {
|
|
7
|
+
Object.defineProperty(this, "data", {
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
writable: true,
|
|
11
|
+
value: void 0
|
|
12
|
+
});
|
|
13
|
+
Object.defineProperty(this, "locked", {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
value: false
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(this, "waitQueue", {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: []
|
|
24
|
+
});
|
|
25
|
+
this.data = initialData;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Acquire the lock. Returns a promise that resolves when the lock is acquired.
|
|
29
|
+
*/
|
|
30
|
+
async acquire() {
|
|
31
|
+
if (!this.locked) {
|
|
32
|
+
this.locked = true;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
return new Promise(resolve => {
|
|
36
|
+
this.waitQueue.push(resolve);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Release the lock, allowing the next waiter to proceed.
|
|
41
|
+
*/
|
|
42
|
+
release() {
|
|
43
|
+
const next = this.waitQueue.shift();
|
|
44
|
+
if (next) {
|
|
45
|
+
next();
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
this.locked = false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Run a function with exclusive access to the protected data.
|
|
53
|
+
* The lock is acquired before the function runs and released after it completes.
|
|
54
|
+
*
|
|
55
|
+
* @param fn - Function that receives the current data and returns updated data or a promise of updated data
|
|
56
|
+
* @returns Promise that resolves with the function's return value
|
|
57
|
+
*/
|
|
58
|
+
async runExclusive(fn) {
|
|
59
|
+
await this.acquire();
|
|
60
|
+
try {
|
|
61
|
+
const result = await fn(this.data);
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
this.release();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Run a function with exclusive access and update the protected data.
|
|
70
|
+
* The lock is acquired before the function runs and released after it completes.
|
|
71
|
+
*
|
|
72
|
+
* @param fn - Function that receives the current data and returns the updated data
|
|
73
|
+
*/
|
|
74
|
+
async update(fn) {
|
|
75
|
+
await this.acquire();
|
|
76
|
+
try {
|
|
77
|
+
this.data = await fn(this.data);
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
this.release();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get a snapshot of the current data without acquiring the lock.
|
|
85
|
+
* Use with caution - this does not guarantee consistency.
|
|
86
|
+
* Prefer runExclusive for reads that need to be consistent with writes.
|
|
87
|
+
*/
|
|
88
|
+
getSnapshot() {
|
|
89
|
+
return this.data;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Create initial session state data with default values.
|
|
94
|
+
*/
|
|
95
|
+
export function createInitialSessionStateData() {
|
|
96
|
+
return {
|
|
97
|
+
state: 'busy',
|
|
98
|
+
pendingState: undefined,
|
|
99
|
+
pendingStateStart: undefined,
|
|
100
|
+
autoApprovalFailed: false,
|
|
101
|
+
autoApprovalReason: undefined,
|
|
102
|
+
autoApprovalAbortController: undefined,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -71,7 +71,9 @@ export function extractBranchParts(branchName) {
|
|
|
71
71
|
export function prepareWorktreeItems(worktrees, sessions) {
|
|
72
72
|
return worktrees.map(wt => {
|
|
73
73
|
const session = sessions.find(s => s.worktreePath === wt.path);
|
|
74
|
-
const status = session
|
|
74
|
+
const status = session
|
|
75
|
+
? ` [${getStatusDisplay(session.stateMutex.getSnapshot().state)}]`
|
|
76
|
+
: '';
|
|
75
77
|
const fullBranchName = wt.branch
|
|
76
78
|
? wt.branch.replace('refs/heads/', '')
|
|
77
79
|
: 'detached';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { generateWorktreeDirectory, extractBranchParts, truncateString, prepareWorktreeItems, calculateColumnPositions, assembleWorktreeLabel, } from './worktreeUtils.js';
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
|
+
import { Mutex, createInitialSessionStateData } from './mutex.js';
|
|
4
5
|
// Mock child_process module
|
|
5
6
|
vi.mock('child_process');
|
|
6
7
|
describe('generateWorktreeDirectory', () => {
|
|
@@ -121,7 +122,6 @@ describe('prepareWorktreeItems', () => {
|
|
|
121
122
|
const mockSession = {
|
|
122
123
|
id: 'test-session',
|
|
123
124
|
worktreePath: '/path/to/worktree',
|
|
124
|
-
state: 'idle',
|
|
125
125
|
process: {},
|
|
126
126
|
output: [],
|
|
127
127
|
outputHistory: [],
|
|
@@ -133,9 +133,10 @@ describe('prepareWorktreeItems', () => {
|
|
|
133
133
|
commandConfig: undefined,
|
|
134
134
|
detectionStrategy: 'claude',
|
|
135
135
|
devcontainerConfig: undefined,
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
stateMutex: new Mutex({
|
|
137
|
+
...createInitialSessionStateData(),
|
|
138
|
+
state: 'idle',
|
|
139
|
+
}),
|
|
139
140
|
};
|
|
140
141
|
it('should prepare basic worktree without git status', () => {
|
|
141
142
|
const items = prepareWorktreeItems([mockWorktree], []);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "3.1
|
|
3
|
+
"version": "3.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",
|
|
@@ -17,41 +17,40 @@
|
|
|
17
17
|
"cli"
|
|
18
18
|
],
|
|
19
19
|
"bin": {
|
|
20
|
-
"ccmanager": "
|
|
20
|
+
"ccmanager": "bin/cli.js"
|
|
21
21
|
},
|
|
22
22
|
"type": "module",
|
|
23
|
-
"engines": {
|
|
24
|
-
"node": ">=22"
|
|
25
|
-
},
|
|
26
23
|
"scripts": {
|
|
27
|
-
"build": "tsc",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
24
|
+
"build": "bun run tsc",
|
|
25
|
+
"build:binary": "bun run scripts/build-binaries.ts",
|
|
26
|
+
"build:binary:native": "bun run scripts/build-binaries.ts --target=native",
|
|
27
|
+
"build:binary:all": "bun run scripts/build-binaries.ts --target=all",
|
|
28
|
+
"dev": "bun run tsc --watch",
|
|
29
|
+
"start": "bun dist/cli.js",
|
|
30
30
|
"test": "vitest --run",
|
|
31
|
-
"lint": "eslint src",
|
|
32
|
-
"lint:fix": "eslint src --fix",
|
|
33
|
-
"typecheck": "tsc --noEmit",
|
|
34
|
-
"prepublishOnly": "
|
|
35
|
-
"prepare": "
|
|
31
|
+
"lint": "bun run eslint src",
|
|
32
|
+
"lint:fix": "bun run eslint src --fix",
|
|
33
|
+
"typecheck": "bun run tsc --noEmit",
|
|
34
|
+
"prepublishOnly": "bun run lint && bun run typecheck && bun run test && bun run build",
|
|
35
|
+
"prepare": "bun run build",
|
|
36
|
+
"publish:packages": "bun run scripts/publish-packages.ts",
|
|
37
|
+
"publish:packages:dry": "bun run scripts/publish-packages.ts --dry-run"
|
|
36
38
|
},
|
|
37
39
|
"files": [
|
|
38
|
-
"dist"
|
|
40
|
+
"dist",
|
|
41
|
+
"bin"
|
|
39
42
|
],
|
|
40
|
-
"
|
|
41
|
-
"@
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"meow": "^11.0.0",
|
|
47
|
-
"node-pty": "^1.0.0",
|
|
48
|
-
"react": "^18.2.0",
|
|
49
|
-
"strip-ansi": "^7.1.0"
|
|
43
|
+
"optionalDependencies": {
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "3.2.1",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "3.2.1",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "3.2.1",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "3.2.1",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "3.2.1"
|
|
50
49
|
},
|
|
51
50
|
"devDependencies": {
|
|
52
51
|
"@eslint/js": "^9.28.0",
|
|
53
52
|
"@sindresorhus/tsconfig": "^3.0.1",
|
|
54
|
-
"@types/
|
|
53
|
+
"@types/bun": "latest",
|
|
55
54
|
"@types/react": "^18.0.32",
|
|
56
55
|
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
|
57
56
|
"@typescript-eslint/parser": "^8.33.1",
|
|
@@ -62,11 +61,22 @@
|
|
|
62
61
|
"eslint-plugin-prettier": "^5.4.1",
|
|
63
62
|
"eslint-plugin-react": "^7.32.2",
|
|
64
63
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
65
|
-
"ink-testing-library": "
|
|
64
|
+
"ink-testing-library": "4.0.0",
|
|
66
65
|
"prettier": "^3.0.0",
|
|
67
|
-
"ts-node": "^10.9.1",
|
|
68
66
|
"typescript": "^5.0.3",
|
|
69
|
-
"vitest": "^
|
|
67
|
+
"vitest": "^4.0.16"
|
|
70
68
|
},
|
|
71
|
-
"prettier": "@vdemedes/prettier-config"
|
|
69
|
+
"prettier": "@vdemedes/prettier-config",
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"@xterm/headless": "^5.5.0",
|
|
72
|
+
"effect": "^3.18.2",
|
|
73
|
+
"ink": "5.2.1",
|
|
74
|
+
"ink-select-input": "^6.0.0",
|
|
75
|
+
"ink-text-input": "^6.0.0",
|
|
76
|
+
"meow": "^11.0.0",
|
|
77
|
+
"react": "18.3.1",
|
|
78
|
+
"react-devtools-core": "^4.19.1",
|
|
79
|
+
"react-dom": "18.3.1",
|
|
80
|
+
"strip-ansi": "^7.1.0"
|
|
81
|
+
}
|
|
72
82
|
}
|