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