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 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 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
@@ -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 'node-pty';
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('node-pty', () => ({
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
- buffer: {
56
- active: {
57
- length: 0,
58
- getLine: vi.fn(),
56
+ Terminal: vi.fn().mockImplementation(function () {
57
+ return {
58
+ buffer: {
59
+ active: {
60
+ length: 0,
61
+ getLine: vi.fn(),
62
+ },
59
63
  },
60
- },
61
- write: vi.fn(),
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 'node-pty';
3
+ import { spawn } from './bunTerminal.js';
4
4
  import { EventEmitter } from 'events';
5
- // Mock node-pty
6
- vi.mock('node-pty', () => ({
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
- buffer: {
29
- active: {
30
- length: 0,
31
- getLine: vi.fn(),
29
+ Terminal: vi.fn().mockImplementation(function () {
30
+ return {
31
+ buffer: {
32
+ active: {
33
+ length: 0,
34
+ getLine: vi.fn(),
35
+ },
32
36
  },
33
- },
34
- write: vi.fn(),
35
- })),
37
+ write: vi.fn(),
38
+ };
39
+ }),
36
40
  },
37
41
  }));
38
42
  // Create a mock IPty class
@@ -1,4 +1,4 @@
1
- import { spawn } from 'node-pty';
1
+ import { spawn } from './bunTerminal.js';
2
2
  import { EventEmitter } from 'events';
3
3
  import pkg from '@xterm/headless';
4
4
  import { exec } from 'child_process';
@@ -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 'node-pty';
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('node-pty', () => ({
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 'node-pty';
3
+ import { spawn } from './bunTerminal.js';
4
4
  import { EventEmitter } from 'events';
5
5
  import { exec } from 'child_process';
6
- // Mock node-pty
7
- vi.mock('node-pty', () => ({
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
- execFile: vi.fn(),
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().mockImplementation(() => ({
33
- buffer: {
34
- active: {
35
- length: 0,
36
- getLine: vi.fn(),
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
- write: vi.fn(),
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 {
@@ -1,4 +1,4 @@
1
- import { IPty } from 'node-pty';
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
4
  import { Mutex, SessionStateData } from '../utils/mutex.js';
@@ -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
- getWorktreesEffect: vi.fn(() => Effect.succeed([
278
- {
279
- path: tmpDir,
280
- branch: 'test-branch',
281
- isMainWorktree: false,
282
- hasSession: true,
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
- getWorktreesEffect: vi.fn(() => Effect.succeed([
330
- {
331
- path: tmpDir,
332
- branch: 'test-branch',
333
- isMainWorktree: false,
334
- hasSession: true,
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
- getWorktreesEffect: vi.fn(() => Effect.succeed([
380
- {
381
- path: tmpDir,
382
- branch: 'test-branch',
383
- isMainWorktree: false,
384
- hasSession: true,
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
- getWorktreesEffect: vi.fn(() => Effect.fail(new GitError({
432
- command: 'git worktree list --porcelain',
433
- exitCode: 128,
434
- stderr: 'not a git repository',
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.1.5",
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": "dist/cli.js"
20
+ "ccmanager": "bin/cli.js"
21
21
  },
22
22
  "type": "module",
23
- "engines": {
24
- "node": ">=22"
25
- },
26
23
  "scripts": {
27
- "build": "tsc",
28
- "dev": "tsc --watch",
29
- "start": "node dist/cli.js",
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": "npm run lint && npm run typecheck && npm run test && npm run build",
35
- "prepare": "npm run build"
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
- "dependencies": {
41
- "@xterm/headless": "^5.5.0",
42
- "effect": "^3.18.2",
43
- "ink": "^4.1.0",
44
- "ink-select-input": "^5.0.0",
45
- "ink-text-input": "^5.0.1",
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/node": "^20.0.0",
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": "^3.0.0",
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": "^3.2.2"
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
  }