ccmanager 3.1.5 → 3.2.2
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 +86 -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/services/bunTerminal.d.ts +53 -0
- package/dist/services/bunTerminal.js +175 -0
- package/dist/services/sessionManager.autoApproval.test.js +15 -11
- package/dist/services/sessionManager.effect.test.js +16 -12
- package/dist/services/sessionManager.js +1 -1
- package/dist/services/sessionManager.statePersistence.test.js +5 -3
- package/dist/services/sessionManager.test.js +29 -15
- package/dist/types/index.d.ts +1 -1
- package/dist/utils/hookExecutor.test.js +50 -38
- package/package.json +39 -29
package/bin/cli.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
|
|
12
|
+
const PACKAGE_SCOPE = "@kodaikabasawa";
|
|
13
|
+
const PACKAGE_NAME = "ccmanager";
|
|
14
|
+
const BINARY_NAME = "ccmanager";
|
|
15
|
+
|
|
16
|
+
const PLATFORM_PACKAGES = {
|
|
17
|
+
"darwin-arm64": `${PACKAGE_SCOPE}/${PACKAGE_NAME}-darwin-arm64`,
|
|
18
|
+
"darwin-x64": `${PACKAGE_SCOPE}/${PACKAGE_NAME}-darwin-x64`,
|
|
19
|
+
"linux-arm64": `${PACKAGE_SCOPE}/${PACKAGE_NAME}-linux-arm64`,
|
|
20
|
+
"linux-x64": `${PACKAGE_SCOPE}/${PACKAGE_NAME}-linux-x64`,
|
|
21
|
+
"win32-x64": `${PACKAGE_SCOPE}/${PACKAGE_NAME}-win32-x64`,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function getPlatformKey() {
|
|
25
|
+
const platform = process.platform;
|
|
26
|
+
const arch = process.arch;
|
|
27
|
+
return `${platform}-${arch}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getBinaryName() {
|
|
31
|
+
return process.platform === "win32" ? `${BINARY_NAME}.exe` : BINARY_NAME;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getBinaryPath() {
|
|
35
|
+
const platformKey = getPlatformKey();
|
|
36
|
+
const platformPackage = PLATFORM_PACKAGES[platformKey];
|
|
37
|
+
const binaryName = getBinaryName();
|
|
38
|
+
|
|
39
|
+
if (!platformPackage) {
|
|
40
|
+
console.error(`Unsupported platform: ${platformKey}`);
|
|
41
|
+
console.error(
|
|
42
|
+
`Supported platforms: ${Object.keys(PLATFORM_PACKAGES).join(", ")}`,
|
|
43
|
+
);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Try to resolve from platform-specific package (installed via optionalDependencies)
|
|
48
|
+
try {
|
|
49
|
+
const packagePath = dirname(
|
|
50
|
+
require.resolve(`${platformPackage}/package.json`),
|
|
51
|
+
);
|
|
52
|
+
const binaryPath = join(packagePath, "bin", binaryName);
|
|
53
|
+
if (existsSync(binaryPath)) {
|
|
54
|
+
return binaryPath;
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// Platform package not installed, continue to fallback
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fallback: check if binary was downloaded by postinstall script
|
|
61
|
+
const fallbackPath = join(__dirname, binaryName);
|
|
62
|
+
if (existsSync(fallbackPath)) {
|
|
63
|
+
return fallbackPath;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.error(`Could not find ${BINARY_NAME} binary for ${platformKey}`);
|
|
67
|
+
console.error("Please try reinstalling the package:");
|
|
68
|
+
console.error(` npm install -g ${PACKAGE_NAME}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const binaryPath = getBinaryPath();
|
|
74
|
+
const args = process.argv.slice(2);
|
|
75
|
+
|
|
76
|
+
execFileSync(binaryPath, args, {
|
|
77
|
+
stdio: "inherit",
|
|
78
|
+
env: process.env,
|
|
79
|
+
});
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error.status !== undefined) {
|
|
82
|
+
process.exit(error.status);
|
|
83
|
+
}
|
|
84
|
+
console.error("Failed to execute ccmanager:", error.message);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
package/dist/cli.test.js
CHANGED
|
@@ -5,12 +5,11 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
import { dirname } from 'path';
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = dirname(__filename);
|
|
8
|
-
// Check if
|
|
9
|
-
function
|
|
8
|
+
// Check if Bun.Terminal API is available (requires Bun v1.3.5+)
|
|
9
|
+
function isBunTerminalAvailable() {
|
|
10
10
|
try {
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
return true;
|
|
11
|
+
// Check if Bun.Terminal is available
|
|
12
|
+
return typeof Bun !== 'undefined' && typeof Bun.spawn === 'function';
|
|
14
13
|
}
|
|
15
14
|
catch {
|
|
16
15
|
return false;
|
|
@@ -25,7 +24,7 @@ describe('CLI', () => {
|
|
|
25
24
|
process.env = originalEnv;
|
|
26
25
|
});
|
|
27
26
|
describe('--multi-project flag', () => {
|
|
28
|
-
it.skipIf(!
|
|
27
|
+
it.skipIf(!isBunTerminalAvailable())('should exit with error when CCMANAGER_MULTI_PROJECT_ROOT is not set', async () => {
|
|
29
28
|
// Ensure the env var is not set
|
|
30
29
|
delete process.env['CCMANAGER_MULTI_PROJECT_ROOT'];
|
|
31
30
|
// Create a wrapper script that mocks TTY
|
|
@@ -33,11 +32,11 @@ describe('CLI', () => {
|
|
|
33
32
|
process.stdin.isTTY = true;
|
|
34
33
|
process.stdout.isTTY = true;
|
|
35
34
|
process.stderr.isTTY = true;
|
|
36
|
-
process.argv = ['
|
|
35
|
+
process.argv = ['bun', 'cli.js', '--multi-project'];
|
|
37
36
|
import('./cli.js');
|
|
38
37
|
`;
|
|
39
38
|
const result = await new Promise(resolve => {
|
|
40
|
-
const proc = spawn('
|
|
39
|
+
const proc = spawn('bun', ['-e', wrapperScript], {
|
|
41
40
|
cwd: path.join(__dirname, '../dist'),
|
|
42
41
|
env: { ...process.env },
|
|
43
42
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -54,12 +53,12 @@ describe('CLI', () => {
|
|
|
54
53
|
expect(result.stderr).toContain('CCMANAGER_MULTI_PROJECT_ROOT environment variable must be set');
|
|
55
54
|
expect(result.stderr).toContain('export CCMANAGER_MULTI_PROJECT_ROOT=/path/to/projects');
|
|
56
55
|
});
|
|
57
|
-
it.skipIf(!
|
|
56
|
+
it.skipIf(!isBunTerminalAvailable())('should not check for env var when --multi-project is not used', async () => {
|
|
58
57
|
// Ensure the env var is not set
|
|
59
58
|
delete process.env['CCMANAGER_MULTI_PROJECT_ROOT'];
|
|
60
59
|
const result = await new Promise(resolve => {
|
|
61
60
|
const cliPath = path.join(__dirname, '../dist/cli.js');
|
|
62
|
-
const proc = spawn('
|
|
61
|
+
const proc = spawn('bun', [cliPath, '--help'], {
|
|
63
62
|
env: { ...process.env },
|
|
64
63
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
65
64
|
});
|
|
@@ -95,10 +95,12 @@ vi.mock('../services/configurationManager.js', () => ({
|
|
|
95
95
|
configurationManager: configurationManagerMock,
|
|
96
96
|
}));
|
|
97
97
|
vi.mock('../services/worktreeService.js', () => ({
|
|
98
|
-
WorktreeService: vi.fn(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
WorktreeService: vi.fn(function () {
|
|
99
|
+
return {
|
|
100
|
+
createWorktreeEffect: (...args) => createWorktreeEffectMock(...args),
|
|
101
|
+
deleteWorktreeEffect: (...args) => deleteWorktreeEffectMock(...args),
|
|
102
|
+
};
|
|
103
|
+
}),
|
|
102
104
|
}));
|
|
103
105
|
vi.mock('./Menu.js', createInkMock('Menu View', props => (menuProps = props)));
|
|
104
106
|
vi.mock('./ProjectList.js', createInkMock('Project List View', () => { }));
|
|
@@ -5,7 +5,13 @@ import { Effect } from 'effect';
|
|
|
5
5
|
import DeleteWorktree from './DeleteWorktree.js';
|
|
6
6
|
import { WorktreeService } from '../services/worktreeService.js';
|
|
7
7
|
import { GitError } from '../types/errors.js';
|
|
8
|
-
vi.mock('../services/worktreeService.js')
|
|
8
|
+
vi.mock('../services/worktreeService.js', () => ({
|
|
9
|
+
WorktreeService: vi.fn(function () {
|
|
10
|
+
return {
|
|
11
|
+
getWorktreesEffect: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
}),
|
|
14
|
+
}));
|
|
9
15
|
vi.mock('../services/shortcutManager.js', () => ({
|
|
10
16
|
shortcutManager: {
|
|
11
17
|
matchesShortcut: vi.fn(),
|
|
@@ -52,9 +58,11 @@ describe('DeleteWorktree - Effect Integration', () => {
|
|
|
52
58
|
];
|
|
53
59
|
const mockEffect = Effect.succeed(mockWorktrees);
|
|
54
60
|
const mockGetWorktreesEffect = vi.fn(() => mockEffect);
|
|
55
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
62
|
+
return {
|
|
63
|
+
getWorktreesEffect: mockGetWorktreesEffect,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
58
66
|
const onComplete = vi.fn();
|
|
59
67
|
const onCancel = vi.fn();
|
|
60
68
|
// WHEN: Component is rendered
|
|
@@ -77,9 +85,11 @@ describe('DeleteWorktree - Effect Integration', () => {
|
|
|
77
85
|
});
|
|
78
86
|
const mockEffect = Effect.fail(mockError);
|
|
79
87
|
const mockGetWorktreesEffect = vi.fn(() => mockEffect);
|
|
80
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
89
|
+
return {
|
|
90
|
+
getWorktreesEffect: mockGetWorktreesEffect,
|
|
91
|
+
};
|
|
92
|
+
});
|
|
83
93
|
const onComplete = vi.fn();
|
|
84
94
|
const onCancel = vi.fn();
|
|
85
95
|
// WHEN: Component is rendered
|
|
@@ -111,9 +121,11 @@ describe('DeleteWorktree - Effect Integration', () => {
|
|
|
111
121
|
];
|
|
112
122
|
const mockEffect = Effect.succeed(mockWorktrees);
|
|
113
123
|
const mockGetWorktreesEffect = vi.fn(() => mockEffect);
|
|
114
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
125
|
+
return {
|
|
126
|
+
getWorktreesEffect: mockGetWorktreesEffect,
|
|
127
|
+
};
|
|
128
|
+
});
|
|
117
129
|
const onComplete = vi.fn();
|
|
118
130
|
const onCancel = vi.fn();
|
|
119
131
|
// WHEN: Component is rendered
|
|
@@ -5,6 +5,28 @@ import { Effect } from 'effect';
|
|
|
5
5
|
import Menu from './Menu.js';
|
|
6
6
|
import { SessionManager } from '../services/sessionManager.js';
|
|
7
7
|
import { projectManager } from '../services/projectManager.js';
|
|
8
|
+
// Mock bunTerminal to avoid native module issues in tests
|
|
9
|
+
vi.mock('../services/bunTerminal.js', () => ({
|
|
10
|
+
spawn: vi.fn(function () {
|
|
11
|
+
return null;
|
|
12
|
+
}),
|
|
13
|
+
}));
|
|
14
|
+
// Mock @xterm/headless
|
|
15
|
+
vi.mock('@xterm/headless', () => ({
|
|
16
|
+
default: {
|
|
17
|
+
Terminal: vi.fn().mockImplementation(function () {
|
|
18
|
+
return {
|
|
19
|
+
buffer: {
|
|
20
|
+
active: {
|
|
21
|
+
length: 0,
|
|
22
|
+
getLine: vi.fn(),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
write: vi.fn(),
|
|
26
|
+
};
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
8
30
|
// Import the actual component code but skip the useInput hook
|
|
9
31
|
vi.mock('ink', async () => {
|
|
10
32
|
const actual = await vi.importActual('ink');
|
|
@@ -4,9 +4,11 @@ import Menu from './Menu.js';
|
|
|
4
4
|
import { SessionManager } from '../services/sessionManager.js';
|
|
5
5
|
import { WorktreeService } from '../services/worktreeService.js';
|
|
6
6
|
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
7
|
-
// Mock
|
|
8
|
-
vi.mock('
|
|
9
|
-
spawn: vi.fn()
|
|
7
|
+
// Mock bunTerminal to avoid native module issues in tests
|
|
8
|
+
vi.mock('../services/bunTerminal.js', () => ({
|
|
9
|
+
spawn: vi.fn(function () {
|
|
10
|
+
return null;
|
|
11
|
+
}),
|
|
10
12
|
}));
|
|
11
13
|
// Mock ink to avoid stdin issues
|
|
12
14
|
vi.mock('ink', async () => {
|
|
@@ -5,7 +5,13 @@ import { Effect } from 'effect';
|
|
|
5
5
|
import MergeWorktree from './MergeWorktree.js';
|
|
6
6
|
import { WorktreeService } from '../services/worktreeService.js';
|
|
7
7
|
import { GitError } from '../types/errors.js';
|
|
8
|
-
vi.mock('../services/worktreeService.js')
|
|
8
|
+
vi.mock('../services/worktreeService.js', () => ({
|
|
9
|
+
WorktreeService: vi.fn(function () {
|
|
10
|
+
return {
|
|
11
|
+
getWorktreesEffect: vi.fn(),
|
|
12
|
+
};
|
|
13
|
+
}),
|
|
14
|
+
}));
|
|
9
15
|
vi.mock('../services/shortcutManager.js', () => ({
|
|
10
16
|
shortcutManager: {
|
|
11
17
|
matchesShortcut: vi.fn(),
|
|
@@ -52,9 +58,11 @@ describe('MergeWorktree - Effect Integration', () => {
|
|
|
52
58
|
];
|
|
53
59
|
const mockEffect = Effect.succeed(mockWorktrees);
|
|
54
60
|
const mockGetWorktreesEffect = vi.fn(() => mockEffect);
|
|
55
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
62
|
+
return {
|
|
63
|
+
getWorktreesEffect: mockGetWorktreesEffect,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
58
66
|
const onComplete = vi.fn();
|
|
59
67
|
const onCancel = vi.fn();
|
|
60
68
|
// WHEN: Component is rendered
|
|
@@ -2,9 +2,11 @@ import React from 'react';
|
|
|
2
2
|
import { render } from 'ink-testing-library';
|
|
3
3
|
import NewWorktree from './NewWorktree.js';
|
|
4
4
|
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
|
-
// Mock
|
|
6
|
-
vi.mock('
|
|
7
|
-
spawn: vi.fn()
|
|
5
|
+
// Mock bunTerminal to avoid native module issues in tests
|
|
6
|
+
vi.mock('../services/bunTerminal.js', () => ({
|
|
7
|
+
spawn: vi.fn(function () {
|
|
8
|
+
return null;
|
|
9
|
+
}),
|
|
8
10
|
}));
|
|
9
11
|
// Mock ink to avoid stdin issues
|
|
10
12
|
vi.mock('ink', async () => {
|
|
@@ -60,7 +62,9 @@ vi.mock('../hooks/useSearchMode.js', () => ({
|
|
|
60
62
|
}));
|
|
61
63
|
// Mock WorktreeService
|
|
62
64
|
vi.mock('../services/worktreeService.js', () => ({
|
|
63
|
-
WorktreeService: vi.fn()
|
|
65
|
+
WorktreeService: vi.fn(function () {
|
|
66
|
+
return {};
|
|
67
|
+
}),
|
|
64
68
|
}));
|
|
65
69
|
describe('NewWorktree component Effect integration', () => {
|
|
66
70
|
beforeEach(() => {
|
|
@@ -73,14 +77,16 @@ describe('NewWorktree component Effect integration', () => {
|
|
|
73
77
|
const { Effect } = await import('effect');
|
|
74
78
|
const { WorktreeService } = await import('../services/worktreeService.js');
|
|
75
79
|
// Mock WorktreeService to return Effects that never resolve (simulating loading)
|
|
76
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
81
|
+
return {
|
|
82
|
+
getAllBranchesEffect: vi.fn(() => Effect.async(() => {
|
|
83
|
+
// Never resolves to simulate loading state
|
|
84
|
+
})),
|
|
85
|
+
getDefaultBranchEffect: vi.fn(() => Effect.async(() => {
|
|
86
|
+
// Never resolves to simulate loading state
|
|
87
|
+
})),
|
|
88
|
+
};
|
|
89
|
+
});
|
|
84
90
|
const onComplete = vi.fn();
|
|
85
91
|
const onCancel = vi.fn();
|
|
86
92
|
const { lastFrame } = render(React.createElement(NewWorktree, { onComplete: onComplete, onCancel: onCancel }));
|
|
@@ -100,10 +106,12 @@ describe('NewWorktree component Effect integration', () => {
|
|
|
100
106
|
stdout: '',
|
|
101
107
|
});
|
|
102
108
|
// Mock WorktreeService to fail with GitError
|
|
103
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
110
|
+
return {
|
|
111
|
+
getAllBranchesEffect: vi.fn(() => Effect.fail(gitError)),
|
|
112
|
+
getDefaultBranchEffect: vi.fn(() => Effect.succeed('main')),
|
|
113
|
+
};
|
|
114
|
+
});
|
|
107
115
|
const onComplete = vi.fn();
|
|
108
116
|
const onCancel = vi.fn();
|
|
109
117
|
const { lastFrame } = render(React.createElement(NewWorktree, { onComplete: onComplete, onCancel: onCancel }));
|
|
@@ -122,10 +130,12 @@ describe('NewWorktree component Effect integration', () => {
|
|
|
122
130
|
// Mock WorktreeService to succeed with both Effects
|
|
123
131
|
const getAllBranchesSpy = vi.fn(() => Effect.succeed(mockBranches));
|
|
124
132
|
const getDefaultBranchSpy = vi.fn(() => Effect.succeed(mockDefaultBranch));
|
|
125
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
133
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
134
|
+
return {
|
|
135
|
+
getAllBranchesEffect: getAllBranchesSpy,
|
|
136
|
+
getDefaultBranchEffect: getDefaultBranchSpy,
|
|
137
|
+
};
|
|
138
|
+
});
|
|
129
139
|
const onComplete = vi.fn();
|
|
130
140
|
const onCancel = vi.fn();
|
|
131
141
|
render(React.createElement(NewWorktree, { onComplete: onComplete, onCancel: onCancel }));
|
|
@@ -146,10 +156,12 @@ describe('NewWorktree component Effect integration', () => {
|
|
|
146
156
|
stdout: '',
|
|
147
157
|
});
|
|
148
158
|
// Mock WorktreeService - branches succeed, default branch fails
|
|
149
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
159
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
160
|
+
return {
|
|
161
|
+
getAllBranchesEffect: vi.fn(() => Effect.succeed(['main', 'develop'])),
|
|
162
|
+
getDefaultBranchEffect: vi.fn(() => Effect.fail(gitError)),
|
|
163
|
+
};
|
|
164
|
+
});
|
|
153
165
|
const onComplete = vi.fn();
|
|
154
166
|
const onCancel = vi.fn();
|
|
155
167
|
const { lastFrame } = render(React.createElement(NewWorktree, { onComplete: onComplete, onCancel: onCancel }));
|
|
@@ -171,10 +183,12 @@ describe('NewWorktree component Effect integration', () => {
|
|
|
171
183
|
copySessionData: true,
|
|
172
184
|
});
|
|
173
185
|
// Mock WorktreeService to return empty branch list
|
|
174
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
186
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
187
|
+
return {
|
|
188
|
+
getAllBranchesEffect: vi.fn(() => Effect.succeed([])),
|
|
189
|
+
getDefaultBranchEffect: vi.fn(() => Effect.succeed('main')),
|
|
190
|
+
};
|
|
191
|
+
});
|
|
178
192
|
const onComplete = vi.fn();
|
|
179
193
|
const onCancel = vi.fn();
|
|
180
194
|
const { lastFrame } = render(React.createElement(NewWorktree, { onComplete: onComplete, onCancel: onCancel }));
|
|
@@ -199,10 +213,12 @@ describe('NewWorktree component Effect integration', () => {
|
|
|
199
213
|
const mockBranches = ['main', 'feature-1', 'develop'];
|
|
200
214
|
const mockDefaultBranch = 'main';
|
|
201
215
|
// Mock WorktreeService to succeed
|
|
202
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
216
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
217
|
+
return {
|
|
218
|
+
getAllBranchesEffect: vi.fn(() => Effect.succeed(mockBranches)),
|
|
219
|
+
getDefaultBranchEffect: vi.fn(() => Effect.succeed(mockDefaultBranch)),
|
|
220
|
+
};
|
|
221
|
+
});
|
|
206
222
|
const onComplete = vi.fn();
|
|
207
223
|
const onCancel = vi.fn();
|
|
208
224
|
const { lastFrame } = render(React.createElement(NewWorktree, { onComplete: onComplete, onCancel: onCancel }));
|
|
@@ -226,13 +242,15 @@ describe('NewWorktree component Effect integration', () => {
|
|
|
226
242
|
});
|
|
227
243
|
// Track Effect execution
|
|
228
244
|
let effectExecuted = false;
|
|
229
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
245
|
+
vi.mocked(WorktreeService).mockImplementation(function () {
|
|
246
|
+
return {
|
|
247
|
+
getAllBranchesEffect: vi.fn(() => {
|
|
248
|
+
effectExecuted = true;
|
|
249
|
+
return Effect.fail(gitError);
|
|
250
|
+
}),
|
|
251
|
+
getDefaultBranchEffect: vi.fn(() => Effect.succeed('main')),
|
|
252
|
+
};
|
|
253
|
+
});
|
|
236
254
|
const onComplete = vi.fn();
|
|
237
255
|
const onCancel = vi.fn();
|
|
238
256
|
render(React.createElement(NewWorktree, { onComplete: onComplete, onCancel: onCancel }));
|
|
@@ -4,9 +4,11 @@ import { expect, describe, it, vi, beforeEach, afterEach } from 'vitest';
|
|
|
4
4
|
import ProjectList from './ProjectList.js';
|
|
5
5
|
import { projectManager } from '../services/projectManager.js';
|
|
6
6
|
import { Effect } from 'effect';
|
|
7
|
-
// Mock
|
|
8
|
-
vi.mock('
|
|
9
|
-
spawn: vi.fn()
|
|
7
|
+
// Mock bunTerminal to avoid native module loading issues
|
|
8
|
+
vi.mock('../services/bunTerminal.js', () => ({
|
|
9
|
+
spawn: vi.fn(function () {
|
|
10
|
+
return null;
|
|
11
|
+
}),
|
|
10
12
|
}));
|
|
11
13
|
// Mock ink to avoid stdin.ref issues
|
|
12
14
|
vi.mock('ink', async () => {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render } from 'ink-testing-library';
|
|
3
3
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
4
|
-
// Mock
|
|
5
|
-
vi.mock('
|
|
6
|
-
spawn: vi.fn()
|
|
4
|
+
// Mock bunTerminal to avoid native module loading issues
|
|
5
|
+
vi.mock('../services/bunTerminal.js', () => ({
|
|
6
|
+
spawn: vi.fn(function () {
|
|
7
|
+
return null;
|
|
8
|
+
}),
|
|
7
9
|
}));
|
|
8
10
|
// Import the actual component code but skip the useInput hook
|
|
9
11
|
vi.mock('ink', async () => {
|
|
@@ -224,6 +226,8 @@ describe('ProjectList', () => {
|
|
|
224
226
|
backspace: false,
|
|
225
227
|
delete: false,
|
|
226
228
|
meta: false,
|
|
229
|
+
home: false,
|
|
230
|
+
end: false,
|
|
227
231
|
});
|
|
228
232
|
});
|
|
229
233
|
// Wait a bit for state update
|
|
@@ -262,6 +266,8 @@ describe('ProjectList', () => {
|
|
|
262
266
|
backspace: false,
|
|
263
267
|
delete: false,
|
|
264
268
|
meta: false,
|
|
269
|
+
home: false,
|
|
270
|
+
end: false,
|
|
265
271
|
});
|
|
266
272
|
// Force rerender with search active and query
|
|
267
273
|
rerender(React.createElement(ProjectList, { projectsDir: "/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
@@ -299,6 +305,8 @@ describe('ProjectList', () => {
|
|
|
299
305
|
backspace: false,
|
|
300
306
|
delete: false,
|
|
301
307
|
meta: false,
|
|
308
|
+
home: false,
|
|
309
|
+
end: false,
|
|
302
310
|
});
|
|
303
311
|
// Wait a bit for state update
|
|
304
312
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
@@ -322,6 +330,8 @@ describe('ProjectList', () => {
|
|
|
322
330
|
backspace: false,
|
|
323
331
|
delete: false,
|
|
324
332
|
meta: false,
|
|
333
|
+
home: false,
|
|
334
|
+
end: false,
|
|
325
335
|
});
|
|
326
336
|
// Wait a bit for state update
|
|
327
337
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
@@ -358,6 +368,8 @@ describe('ProjectList', () => {
|
|
|
358
368
|
backspace: false,
|
|
359
369
|
delete: false,
|
|
360
370
|
meta: false,
|
|
371
|
+
home: false,
|
|
372
|
+
end: false,
|
|
361
373
|
});
|
|
362
374
|
// Wait a bit for state update
|
|
363
375
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
@@ -399,6 +411,8 @@ describe('ProjectList', () => {
|
|
|
399
411
|
backspace: false,
|
|
400
412
|
delete: false,
|
|
401
413
|
meta: false,
|
|
414
|
+
home: false,
|
|
415
|
+
end: false,
|
|
402
416
|
});
|
|
403
417
|
// Wait a bit for state update
|
|
404
418
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
@@ -422,6 +436,8 @@ describe('ProjectList', () => {
|
|
|
422
436
|
backspace: false,
|
|
423
437
|
delete: false,
|
|
424
438
|
meta: false,
|
|
439
|
+
home: false,
|
|
440
|
+
end: false,
|
|
425
441
|
});
|
|
426
442
|
// Wait a bit for state update
|
|
427
443
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
@@ -464,6 +480,8 @@ describe('ProjectList', () => {
|
|
|
464
480
|
backspace: false,
|
|
465
481
|
delete: false,
|
|
466
482
|
meta: false,
|
|
483
|
+
home: false,
|
|
484
|
+
end: false,
|
|
467
485
|
});
|
|
468
486
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
469
487
|
// Exit search mode with Enter (keeping filter)
|
|
@@ -482,6 +500,8 @@ describe('ProjectList', () => {
|
|
|
482
500
|
backspace: false,
|
|
483
501
|
delete: false,
|
|
484
502
|
meta: false,
|
|
503
|
+
home: false,
|
|
504
|
+
end: false,
|
|
485
505
|
});
|
|
486
506
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
487
507
|
// Now press ESC outside search mode to clear filter
|
|
@@ -500,6 +520,8 @@ describe('ProjectList', () => {
|
|
|
500
520
|
backspace: false,
|
|
501
521
|
delete: false,
|
|
502
522
|
meta: false,
|
|
523
|
+
home: false,
|
|
524
|
+
end: false,
|
|
503
525
|
});
|
|
504
526
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
505
527
|
// Force rerender
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BunTerminal - A wrapper around Bun's built-in Terminal API
|
|
3
|
+
* that provides an interface compatible with the IPty interface.
|
|
4
|
+
*
|
|
5
|
+
* This replaces @skitee3000/bun-pty to avoid native library issues
|
|
6
|
+
* when running compiled Bun binaries.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Interface for disposable resources.
|
|
10
|
+
*/
|
|
11
|
+
export interface IDisposable {
|
|
12
|
+
dispose(): void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Exit event data for PTY process.
|
|
16
|
+
*/
|
|
17
|
+
export interface IExitEvent {
|
|
18
|
+
exitCode: number;
|
|
19
|
+
signal?: number | string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Options for spawning a new PTY process.
|
|
23
|
+
*/
|
|
24
|
+
export interface IPtyForkOptions {
|
|
25
|
+
name: string;
|
|
26
|
+
cols?: number;
|
|
27
|
+
rows?: number;
|
|
28
|
+
cwd?: string;
|
|
29
|
+
env?: Record<string, string | undefined>;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Interface for interacting with a pseudo-terminal (PTY) instance.
|
|
33
|
+
*/
|
|
34
|
+
export interface IPty {
|
|
35
|
+
readonly pid: number;
|
|
36
|
+
readonly cols: number;
|
|
37
|
+
readonly rows: number;
|
|
38
|
+
readonly process: string;
|
|
39
|
+
readonly onData: (listener: (data: string) => void) => IDisposable;
|
|
40
|
+
readonly onExit: (listener: (event: IExitEvent) => void) => IDisposable;
|
|
41
|
+
write(data: string): void;
|
|
42
|
+
resize(columns: number, rows: number): void;
|
|
43
|
+
kill(signal?: string): void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Spawn a new PTY process using Bun's built-in Terminal API.
|
|
47
|
+
*
|
|
48
|
+
* @param file - The command to execute
|
|
49
|
+
* @param args - Arguments to pass to the command
|
|
50
|
+
* @param options - PTY fork options
|
|
51
|
+
* @returns An IPty instance
|
|
52
|
+
*/
|
|
53
|
+
export declare function spawn(file: string, args: string[], options: IPtyForkOptions): IPty;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BunTerminal - A wrapper around Bun's built-in Terminal API
|
|
3
|
+
* that provides an interface compatible with the IPty interface.
|
|
4
|
+
*
|
|
5
|
+
* This replaces @skitee3000/bun-pty to avoid native library issues
|
|
6
|
+
* when running compiled Bun binaries.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* BunTerminal class that wraps Bun's built-in Terminal API.
|
|
10
|
+
*/
|
|
11
|
+
class BunTerminal {
|
|
12
|
+
constructor(file, args, options) {
|
|
13
|
+
Object.defineProperty(this, "_pid", {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
value: -1
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(this, "_cols", {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: void 0
|
|
24
|
+
});
|
|
25
|
+
Object.defineProperty(this, "_rows", {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
configurable: true,
|
|
28
|
+
writable: true,
|
|
29
|
+
value: void 0
|
|
30
|
+
});
|
|
31
|
+
Object.defineProperty(this, "_process", {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
configurable: true,
|
|
34
|
+
writable: true,
|
|
35
|
+
value: void 0
|
|
36
|
+
});
|
|
37
|
+
Object.defineProperty(this, "_closed", {
|
|
38
|
+
enumerable: true,
|
|
39
|
+
configurable: true,
|
|
40
|
+
writable: true,
|
|
41
|
+
value: false
|
|
42
|
+
});
|
|
43
|
+
Object.defineProperty(this, "_dataListeners", {
|
|
44
|
+
enumerable: true,
|
|
45
|
+
configurable: true,
|
|
46
|
+
writable: true,
|
|
47
|
+
value: []
|
|
48
|
+
});
|
|
49
|
+
Object.defineProperty(this, "_exitListeners", {
|
|
50
|
+
enumerable: true,
|
|
51
|
+
configurable: true,
|
|
52
|
+
writable: true,
|
|
53
|
+
value: []
|
|
54
|
+
});
|
|
55
|
+
Object.defineProperty(this, "_subprocess", {
|
|
56
|
+
enumerable: true,
|
|
57
|
+
configurable: true,
|
|
58
|
+
writable: true,
|
|
59
|
+
value: null
|
|
60
|
+
});
|
|
61
|
+
Object.defineProperty(this, "onData", {
|
|
62
|
+
enumerable: true,
|
|
63
|
+
configurable: true,
|
|
64
|
+
writable: true,
|
|
65
|
+
value: (listener) => {
|
|
66
|
+
this._dataListeners.push(listener);
|
|
67
|
+
return {
|
|
68
|
+
dispose: () => {
|
|
69
|
+
const index = this._dataListeners.indexOf(listener);
|
|
70
|
+
if (index !== -1) {
|
|
71
|
+
this._dataListeners.splice(index, 1);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
Object.defineProperty(this, "onExit", {
|
|
78
|
+
enumerable: true,
|
|
79
|
+
configurable: true,
|
|
80
|
+
writable: true,
|
|
81
|
+
value: (listener) => {
|
|
82
|
+
this._exitListeners.push(listener);
|
|
83
|
+
return {
|
|
84
|
+
dispose: () => {
|
|
85
|
+
const index = this._exitListeners.indexOf(listener);
|
|
86
|
+
if (index !== -1) {
|
|
87
|
+
this._exitListeners.splice(index, 1);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
this._cols = options.cols ?? 80;
|
|
94
|
+
this._rows = options.rows ?? 24;
|
|
95
|
+
this._process = file;
|
|
96
|
+
// Spawn the process with Bun's built-in terminal support
|
|
97
|
+
this._subprocess = Bun.spawn([file, ...args], {
|
|
98
|
+
cwd: options.cwd ?? process.cwd(),
|
|
99
|
+
env: options.env,
|
|
100
|
+
terminal: {
|
|
101
|
+
cols: this._cols,
|
|
102
|
+
rows: this._rows,
|
|
103
|
+
data: (_terminal, data) => {
|
|
104
|
+
if (!this._closed) {
|
|
105
|
+
const str = typeof data === 'string'
|
|
106
|
+
? data
|
|
107
|
+
: Buffer.from(data).toString('utf8');
|
|
108
|
+
for (const listener of this._dataListeners) {
|
|
109
|
+
listener(str);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
this._pid = this._subprocess.pid;
|
|
116
|
+
// Handle process exit
|
|
117
|
+
this._subprocess.exited.then(exitCode => {
|
|
118
|
+
if (!this._closed) {
|
|
119
|
+
this._closed = true;
|
|
120
|
+
for (const listener of this._exitListeners) {
|
|
121
|
+
listener({ exitCode });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
get pid() {
|
|
127
|
+
return this._pid;
|
|
128
|
+
}
|
|
129
|
+
get cols() {
|
|
130
|
+
return this._cols;
|
|
131
|
+
}
|
|
132
|
+
get rows() {
|
|
133
|
+
return this._rows;
|
|
134
|
+
}
|
|
135
|
+
get process() {
|
|
136
|
+
return this._process;
|
|
137
|
+
}
|
|
138
|
+
write(data) {
|
|
139
|
+
if (this._closed || !this._subprocess?.terminal) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
this._subprocess.terminal.write(data);
|
|
143
|
+
}
|
|
144
|
+
resize(columns, rows) {
|
|
145
|
+
if (this._closed || !this._subprocess?.terminal) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this._cols = columns;
|
|
149
|
+
this._rows = rows;
|
|
150
|
+
this._subprocess.terminal.resize(columns, rows);
|
|
151
|
+
}
|
|
152
|
+
kill(_signal) {
|
|
153
|
+
if (this._closed) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this._closed = true;
|
|
157
|
+
if (this._subprocess?.terminal) {
|
|
158
|
+
this._subprocess.terminal.close();
|
|
159
|
+
}
|
|
160
|
+
if (this._subprocess) {
|
|
161
|
+
this._subprocess.kill();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Spawn a new PTY process using Bun's built-in Terminal API.
|
|
167
|
+
*
|
|
168
|
+
* @param file - The command to execute
|
|
169
|
+
* @param args - Arguments to pass to the command
|
|
170
|
+
* @param options - PTY fork options
|
|
171
|
+
* @returns An IPty instance
|
|
172
|
+
*/
|
|
173
|
+
export function spawn(file, args, options) {
|
|
174
|
+
return new BunTerminal(file, args, options);
|
|
175
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
2
|
import { EventEmitter } from 'events';
|
|
3
|
-
import { spawn } from '
|
|
3
|
+
import { spawn } from './bunTerminal.js';
|
|
4
4
|
import { STATE_CHECK_INTERVAL_MS, STATE_PERSISTENCE_DURATION_MS, } from '../constants/statePersistence.js';
|
|
5
5
|
import { Effect } from 'effect';
|
|
6
6
|
const detectStateMock = vi.fn();
|
|
@@ -9,8 +9,10 @@ let verifyResolve = null;
|
|
|
9
9
|
const verifyNeedsPermissionMock = vi.fn(() => Effect.promise(() => new Promise(resolve => {
|
|
10
10
|
verifyResolve = resolve;
|
|
11
11
|
})));
|
|
12
|
-
vi.mock('
|
|
13
|
-
spawn: vi.fn()
|
|
12
|
+
vi.mock('./bunTerminal.js', () => ({
|
|
13
|
+
spawn: vi.fn(function () {
|
|
14
|
+
return null;
|
|
15
|
+
}),
|
|
14
16
|
}));
|
|
15
17
|
vi.mock('./stateDetector.js', () => ({
|
|
16
18
|
createStateDetector: () => ({ detectState: detectStateMock }),
|
|
@@ -51,15 +53,17 @@ vi.mock('./configurationManager.js', () => ({
|
|
|
51
53
|
}));
|
|
52
54
|
vi.mock('@xterm/headless', () => ({
|
|
53
55
|
default: {
|
|
54
|
-
Terminal: vi.fn().mockImplementation(()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
Terminal: vi.fn().mockImplementation(function () {
|
|
57
|
+
return {
|
|
58
|
+
buffer: {
|
|
59
|
+
active: {
|
|
60
|
+
length: 0,
|
|
61
|
+
getLine: vi.fn(),
|
|
62
|
+
},
|
|
59
63
|
},
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
})
|
|
64
|
+
write: vi.fn(),
|
|
65
|
+
};
|
|
66
|
+
}),
|
|
63
67
|
},
|
|
64
68
|
}));
|
|
65
69
|
vi.mock('./autoApprovalVerifier.js', () => ({
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
2
|
import { Effect, Either } from 'effect';
|
|
3
|
-
import { spawn } from '
|
|
3
|
+
import { spawn } from './bunTerminal.js';
|
|
4
4
|
import { EventEmitter } from 'events';
|
|
5
|
-
// Mock
|
|
6
|
-
vi.mock('
|
|
7
|
-
spawn: vi.fn()
|
|
5
|
+
// Mock bunTerminal
|
|
6
|
+
vi.mock('./bunTerminal.js', () => ({
|
|
7
|
+
spawn: vi.fn(function () {
|
|
8
|
+
return null;
|
|
9
|
+
}),
|
|
8
10
|
}));
|
|
9
11
|
// Mock child_process
|
|
10
12
|
vi.mock('child_process', () => ({
|
|
@@ -24,15 +26,17 @@ vi.mock('./configurationManager.js', () => ({
|
|
|
24
26
|
// Mock Terminal
|
|
25
27
|
vi.mock('@xterm/headless', () => ({
|
|
26
28
|
default: {
|
|
27
|
-
Terminal: vi.fn().mockImplementation(()
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
Terminal: vi.fn().mockImplementation(function () {
|
|
30
|
+
return {
|
|
31
|
+
buffer: {
|
|
32
|
+
active: {
|
|
33
|
+
length: 0,
|
|
34
|
+
getLine: vi.fn(),
|
|
35
|
+
},
|
|
32
36
|
},
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
})
|
|
37
|
+
write: vi.fn(),
|
|
38
|
+
};
|
|
39
|
+
}),
|
|
36
40
|
},
|
|
37
41
|
}));
|
|
38
42
|
// Create a mock IPty class
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
2
|
import { SessionManager } from './sessionManager.js';
|
|
3
|
-
import { spawn } from '
|
|
3
|
+
import { spawn } from './bunTerminal.js';
|
|
4
4
|
import { EventEmitter } from 'events';
|
|
5
5
|
import { STATE_PERSISTENCE_DURATION_MS, STATE_CHECK_INTERVAL_MS, } from '../constants/statePersistence.js';
|
|
6
|
-
vi.mock('
|
|
7
|
-
spawn: vi.fn()
|
|
6
|
+
vi.mock('./bunTerminal.js', () => ({
|
|
7
|
+
spawn: vi.fn(function () {
|
|
8
|
+
return null;
|
|
9
|
+
}),
|
|
8
10
|
}));
|
|
9
11
|
vi.mock('./configurationManager.js', () => ({
|
|
10
12
|
configurationManager: {
|
|
@@ -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 {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -16,7 +16,11 @@ vi.mock('../services/configurationManager.js', () => ({
|
|
|
16
16
|
}));
|
|
17
17
|
// Mock the WorktreeService
|
|
18
18
|
vi.mock('../services/worktreeService.js', () => ({
|
|
19
|
-
WorktreeService: vi.fn()
|
|
19
|
+
WorktreeService: vi.fn(function () {
|
|
20
|
+
return {
|
|
21
|
+
getWorktreesEffect: vi.fn(),
|
|
22
|
+
};
|
|
23
|
+
}),
|
|
20
24
|
}));
|
|
21
25
|
// Note: This file contains integration tests that execute real commands
|
|
22
26
|
describe('hookExecutor Integration Tests', () => {
|
|
@@ -273,16 +277,18 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
273
277
|
stateMutex: new Mutex(createInitialSessionStateData()),
|
|
274
278
|
};
|
|
275
279
|
// Mock WorktreeService to return a worktree with the tmpDir path
|
|
276
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
});
|
|
286
292
|
// Configure mock to return a hook that writes to a file with delay
|
|
287
293
|
vi.mocked(configurationManager.getStatusHooks).mockReturnValue({
|
|
288
294
|
busy: {
|
|
@@ -325,16 +331,18 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
325
331
|
stateMutex: new Mutex(createInitialSessionStateData()),
|
|
326
332
|
};
|
|
327
333
|
// Mock WorktreeService to return a worktree with the tmpDir path
|
|
328
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
});
|
|
338
346
|
// Configure mock to return a hook that fails
|
|
339
347
|
vi.mocked(configurationManager.getStatusHooks).mockReturnValue({
|
|
340
348
|
busy: {
|
|
@@ -375,16 +383,18 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
375
383
|
stateMutex: new Mutex(createInitialSessionStateData()),
|
|
376
384
|
};
|
|
377
385
|
// Mock WorktreeService to return a worktree with the tmpDir path
|
|
378
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
+
});
|
|
388
398
|
// Configure mock to return a disabled hook
|
|
389
399
|
vi.mocked(configurationManager.getStatusHooks).mockReturnValue({
|
|
390
400
|
busy: {
|
|
@@ -427,13 +437,15 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
427
437
|
stateMutex: new Mutex(createInitialSessionStateData()),
|
|
428
438
|
};
|
|
429
439
|
// Mock WorktreeService to fail with GitError
|
|
430
|
-
vi.mocked(WorktreeService).mockImplementation(()
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
+
});
|
|
437
449
|
// Configure mock to return a hook that should execute despite worktree query failure
|
|
438
450
|
vi.mocked(configurationManager.getStatusHooks).mockReturnValue({
|
|
439
451
|
busy: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.2",
|
|
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.2",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "3.2.2",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "3.2.2",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "3.2.2",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "3.2.2"
|
|
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
|
}
|