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 ADDED
@@ -0,0 +1,85 @@
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_NAME = "ccmanager";
13
+ const BINARY_NAME = "ccmanager";
14
+
15
+ const PLATFORM_PACKAGES = {
16
+ "darwin-arm64": `${PACKAGE_NAME}-darwin-arm64`,
17
+ "darwin-x64": `${PACKAGE_NAME}-darwin-x64`,
18
+ "linux-arm64": `${PACKAGE_NAME}-linux-arm64`,
19
+ "linux-x64": `${PACKAGE_NAME}-linux-x64`,
20
+ "win32-x64": `${PACKAGE_NAME}-win32-x64`,
21
+ };
22
+
23
+ function getPlatformKey() {
24
+ const platform = process.platform;
25
+ const arch = process.arch;
26
+ return `${platform}-${arch}`;
27
+ }
28
+
29
+ function getBinaryName() {
30
+ return process.platform === "win32" ? `${BINARY_NAME}.exe` : BINARY_NAME;
31
+ }
32
+
33
+ function getBinaryPath() {
34
+ const platformKey = getPlatformKey();
35
+ const platformPackage = PLATFORM_PACKAGES[platformKey];
36
+ const binaryName = getBinaryName();
37
+
38
+ if (!platformPackage) {
39
+ console.error(`Unsupported platform: ${platformKey}`);
40
+ console.error(
41
+ `Supported platforms: ${Object.keys(PLATFORM_PACKAGES).join(", ")}`,
42
+ );
43
+ process.exit(1);
44
+ }
45
+
46
+ // Try to resolve from platform-specific package (installed via optionalDependencies)
47
+ try {
48
+ const packagePath = dirname(
49
+ require.resolve(`${platformPackage}/package.json`),
50
+ );
51
+ const binaryPath = join(packagePath, "bin", binaryName);
52
+ if (existsSync(binaryPath)) {
53
+ return binaryPath;
54
+ }
55
+ } catch {
56
+ // Platform package not installed, continue to fallback
57
+ }
58
+
59
+ // Fallback: check if binary was downloaded by postinstall script
60
+ const fallbackPath = join(__dirname, binaryName);
61
+ if (existsSync(fallbackPath)) {
62
+ return fallbackPath;
63
+ }
64
+
65
+ console.error(`Could not find ${BINARY_NAME} binary for ${platformKey}`);
66
+ console.error("Please try reinstalling the package:");
67
+ console.error(` npm install -g ${PACKAGE_NAME}`);
68
+ process.exit(1);
69
+ }
70
+
71
+ try {
72
+ const binaryPath = getBinaryPath();
73
+ const args = process.argv.slice(2);
74
+
75
+ execFileSync(binaryPath, args, {
76
+ stdio: "inherit",
77
+ env: process.env,
78
+ });
79
+ } catch (error) {
80
+ if (error.status !== undefined) {
81
+ process.exit(error.status);
82
+ }
83
+ console.error("Failed to execute ccmanager:", error.message);
84
+ process.exit(1);
85
+ }
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 node-pty native module is available
9
- function isNodePtyAvailable() {
8
+ // Check if Bun.Terminal API is available (requires Bun v1.3.5+)
9
+ function isBunTerminalAvailable() {
10
10
  try {
11
- // Use eval to bypass linter's require() check
12
- new Function('return require("node-pty")')();
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(!isNodePtyAvailable())('should exit with error when CCMANAGER_MULTI_PROJECT_ROOT is not set', async () => {
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 = ['node', 'cli.js', '--multi-project'];
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('node', ['--input-type=module', '-e', wrapperScript], {
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(!isNodePtyAvailable())('should not check for env var when --multi-project is not used', async () => {
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('node', [cliPath, '--help'], {
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().mockImplementation(() => ({
99
- createWorktreeEffect: (...args) => createWorktreeEffectMock(...args),
100
- deleteWorktreeEffect: (...args) => deleteWorktreeEffectMock(...args),
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
- getWorktreesEffect: mockGetWorktreesEffect,
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
- getWorktreesEffect: mockGetWorktreesEffect,
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
- getWorktreesEffect: mockGetWorktreesEffect,
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 node-pty to avoid native module issues in tests
8
- vi.mock('node-pty', () => ({
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
- getWorktreesEffect: mockGetWorktreesEffect,
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 node-pty to avoid native module issues in tests
6
- vi.mock('node-pty', () => ({
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
- getAllBranchesEffect: vi.fn(() => Effect.async(() => {
78
- // Never resolves to simulate loading state
79
- })),
80
- getDefaultBranchEffect: vi.fn(() => Effect.async(() => {
81
- // Never resolves to simulate loading state
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
- getAllBranchesEffect: vi.fn(() => Effect.fail(gitError)),
105
- getDefaultBranchEffect: vi.fn(() => Effect.succeed('main')),
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
- getAllBranchesEffect: getAllBranchesSpy,
127
- getDefaultBranchEffect: getDefaultBranchSpy,
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
- getAllBranchesEffect: vi.fn(() => Effect.succeed(['main', 'develop'])),
151
- getDefaultBranchEffect: vi.fn(() => Effect.fail(gitError)),
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
- getAllBranchesEffect: vi.fn(() => Effect.succeed([])),
176
- getDefaultBranchEffect: vi.fn(() => Effect.succeed('main')),
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
- getAllBranchesEffect: vi.fn(() => Effect.succeed(mockBranches)),
204
- getDefaultBranchEffect: vi.fn(() => Effect.succeed(mockDefaultBranch)),
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
- getAllBranchesEffect: vi.fn(() => {
231
- effectExecuted = true;
232
- return Effect.fail(gitError);
233
- }),
234
- getDefaultBranchEffect: vi.fn(() => Effect.succeed('main')),
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 node-pty to avoid native module loading issues
8
- vi.mock('node-pty', () => ({
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 node-pty to avoid native module loading issues
5
- vi.mock('node-pty', () => ({
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
@@ -5,17 +5,18 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
5
5
  const { stdout } = useStdout();
6
6
  const [isExiting, setIsExiting] = useState(false);
7
7
  const deriveStatus = (currentSession) => {
8
+ const stateData = currentSession.stateMutex.getSnapshot();
8
9
  // Always prioritize showing the manual approval notice when verification failed
9
- if (currentSession.autoApprovalFailed) {
10
- const reason = currentSession.autoApprovalReason
11
- ? ` Reason: ${currentSession.autoApprovalReason}.`
10
+ if (stateData.autoApprovalFailed) {
11
+ const reason = stateData.autoApprovalReason
12
+ ? ` Reason: ${stateData.autoApprovalReason}.`
12
13
  : '';
13
14
  return {
14
15
  message: `Auto-approval failed.${reason} Manual approval required—respond to the prompt.`,
15
16
  variant: 'error',
16
17
  };
17
18
  }
18
- if (currentSession.state === 'pending_auto_approval') {
19
+ if (stateData.state === 'pending_auto_approval') {
19
20
  return {
20
21
  message: 'Auto-approval pending... verifying permissions (press any key to cancel)',
21
22
  variant: 'pending',
@@ -192,7 +193,7 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
192
193
  onReturnToMenu();
193
194
  return;
194
195
  }
195
- if (session.state === 'pending_auto_approval') {
196
+ if (session.stateMutex.getSnapshot().state === 'pending_auto_approval') {
196
197
  sessionManager.cancelAutoApproval(session.worktreePath, 'User input received during auto-approval');
197
198
  }
198
199
  // Pass all other input directly to the PTY
@@ -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;