ccmanager 1.0.0 → 1.1.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/README.md
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
CCManager is a TUI application for managing multiple AI coding assistant sessions (Claude Code, Gemini CLI) across Git worktrees.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
https://github.com/user-attachments/assets/15914a88-e288-4ac9-94d5-8127f2e19dbf
|
|
8
|
+
|
|
9
|
+
|
|
6
10
|
|
|
7
11
|
## Features
|
|
8
12
|
|
|
@@ -41,23 +45,28 @@ Following Claude Code's philosophy, CCManager keeps things minimal and intuitive
|
|
|
41
45
|
## Install
|
|
42
46
|
|
|
43
47
|
```bash
|
|
44
|
-
|
|
45
|
-
$ npm run build
|
|
46
|
-
$ npm start
|
|
48
|
+
npm install -g ccmanager
|
|
47
49
|
```
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
Or for local development:
|
|
50
52
|
|
|
51
53
|
```bash
|
|
52
|
-
|
|
54
|
+
npm install
|
|
55
|
+
npm run build
|
|
56
|
+
npm start
|
|
53
57
|
```
|
|
54
58
|
|
|
55
|
-
##
|
|
59
|
+
## Usage
|
|
56
60
|
|
|
57
|
-
|
|
61
|
+
```bash
|
|
62
|
+
ccmanager
|
|
63
|
+
```
|
|
58
64
|
|
|
59
|
-
|
|
65
|
+
Or run without installing:
|
|
60
66
|
|
|
67
|
+
```bash
|
|
68
|
+
npx ccmanager
|
|
69
|
+
```
|
|
61
70
|
|
|
62
71
|
## Keyboard Shortcuts
|
|
63
72
|
|
|
@@ -71,7 +80,7 @@ $ npx ccmanager
|
|
|
71
80
|
You can customize keyboard shortcuts in two ways:
|
|
72
81
|
|
|
73
82
|
1. **Through the UI**: Select "Configuration" → "Configure Shortcuts" from the main menu
|
|
74
|
-
2. **Configuration file**: Edit `~/.config/ccmanager/config.json`
|
|
83
|
+
2. **Configuration file**: Edit `~/.config/ccmanager/config.json`
|
|
75
84
|
|
|
76
85
|
Example configuration:
|
|
77
86
|
```json
|
|
@@ -87,17 +96,6 @@ Example configuration:
|
|
|
87
96
|
}
|
|
88
97
|
}
|
|
89
98
|
}
|
|
90
|
-
|
|
91
|
-
// shortcuts.json (legacy format, still supported)
|
|
92
|
-
{
|
|
93
|
-
"returnToMenu": {
|
|
94
|
-
"ctrl": true,
|
|
95
|
-
"key": "r"
|
|
96
|
-
},
|
|
97
|
-
"cancel": {
|
|
98
|
-
"key": "escape"
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
99
|
```
|
|
102
100
|
|
|
103
101
|
Note: Shortcuts from `shortcuts.json` will be automatically migrated to `config.json` on first use.
|
|
@@ -167,7 +165,33 @@ Status hooks allow you to:
|
|
|
167
165
|
- Trigger automations based on session activity
|
|
168
166
|
- Integrate with notification systems like [noti](https://github.com/variadico/noti)
|
|
169
167
|
|
|
170
|
-
For detailed setup instructions, see [docs/state-hooks.md](docs/
|
|
168
|
+
For detailed setup instructions, see [docs/state-hooks.md](docs/status-hooks.md).
|
|
169
|
+
|
|
170
|
+
## Automatic Worktree Directory Generation
|
|
171
|
+
|
|
172
|
+
CCManager can automatically generate worktree directory paths based on branch names, streamlining the worktree creation process.
|
|
173
|
+
|
|
174
|
+
- **Auto-generate paths**: No need to manually specify directories
|
|
175
|
+
- **Customizable patterns**: Use placeholders like `{branch}` in your pattern
|
|
176
|
+
- **Smart sanitization**: Branch names are automatically made filesystem-safe
|
|
177
|
+
|
|
178
|
+
For detailed configuration and examples, see [docs/worktree-auto-directory.md](docs/worktree-auto-directory.md).
|
|
179
|
+
|
|
180
|
+
## Git Worktree Configuration
|
|
181
|
+
|
|
182
|
+
CCManager can display enhanced git status information for each worktree when Git's worktree configuration extension is enabled.
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
# Enable enhanced status tracking
|
|
186
|
+
git config extensions.worktreeConfig true
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
With this enabled, you'll see:
|
|
190
|
+
- **File changes**: `+10 -5` (additions/deletions)
|
|
191
|
+
- **Commit tracking**: `↑3 ↓1` (ahead/behind parent branch)
|
|
192
|
+
- **Parent branch context**: Shows which branch the worktree was created from
|
|
193
|
+
|
|
194
|
+
For complete setup instructions and troubleshooting, see [docs/git-worktree-config.md](docs/git-worktree-config.md).
|
|
171
195
|
|
|
172
196
|
## Development
|
|
173
197
|
|
|
@@ -6,7 +6,14 @@ import { configurationManager } from '../services/configurationManager.js';
|
|
|
6
6
|
import { shortcutManager } from '../services/shortcutManager.js';
|
|
7
7
|
const formatDetectionStrategy = (strategy) => {
|
|
8
8
|
const value = strategy || 'claude';
|
|
9
|
-
|
|
9
|
+
switch (value) {
|
|
10
|
+
case 'gemini':
|
|
11
|
+
return 'Gemini';
|
|
12
|
+
case 'codex':
|
|
13
|
+
return 'Codex';
|
|
14
|
+
default:
|
|
15
|
+
return 'Claude';
|
|
16
|
+
}
|
|
10
17
|
};
|
|
11
18
|
const ConfigureCommand = ({ onComplete }) => {
|
|
12
19
|
const presetsConfig = configurationManager.getCommandPresets();
|
|
@@ -260,6 +267,7 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
260
267
|
const strategyItems = [
|
|
261
268
|
{ label: 'Claude', value: 'claude' },
|
|
262
269
|
{ label: 'Gemini', value: 'gemini' },
|
|
270
|
+
{ label: 'Codex', value: 'codex' },
|
|
263
271
|
];
|
|
264
272
|
const currentStrategy = preset.detectionStrategy || 'claude';
|
|
265
273
|
const initialIndex = strategyItems.findIndex(item => item.value === currentStrategy);
|
|
@@ -308,6 +316,7 @@ const ConfigureCommand = ({ onComplete }) => {
|
|
|
308
316
|
const strategyItems = [
|
|
309
317
|
{ label: 'Claude', value: 'claude' },
|
|
310
318
|
{ label: 'Gemini', value: 'gemini' },
|
|
319
|
+
{ label: 'Codex', value: 'codex' },
|
|
311
320
|
];
|
|
312
321
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
313
322
|
React.createElement(Box, { marginBottom: 1 },
|
|
@@ -14,3 +14,6 @@ export declare class ClaudeStateDetector extends BaseStateDetector {
|
|
|
14
14
|
export declare class GeminiStateDetector extends BaseStateDetector {
|
|
15
15
|
detectState(terminal: Terminal): SessionState;
|
|
16
16
|
}
|
|
17
|
+
export declare class CodexStateDetector extends BaseStateDetector {
|
|
18
|
+
detectState(terminal: Terminal): SessionState;
|
|
19
|
+
}
|
|
@@ -4,6 +4,8 @@ export function createStateDetector(strategy = 'claude') {
|
|
|
4
4
|
return new ClaudeStateDetector();
|
|
5
5
|
case 'gemini':
|
|
6
6
|
return new GeminiStateDetector();
|
|
7
|
+
case 'codex':
|
|
8
|
+
return new CodexStateDetector();
|
|
7
9
|
default:
|
|
8
10
|
return new ClaudeStateDetector();
|
|
9
11
|
}
|
|
@@ -65,3 +67,21 @@ export class GeminiStateDetector extends BaseStateDetector {
|
|
|
65
67
|
return 'idle';
|
|
66
68
|
}
|
|
67
69
|
}
|
|
70
|
+
export class CodexStateDetector extends BaseStateDetector {
|
|
71
|
+
detectState(terminal) {
|
|
72
|
+
const content = this.getTerminalContent(terminal);
|
|
73
|
+
const lowerContent = content.toLowerCase();
|
|
74
|
+
// Check for waiting prompts
|
|
75
|
+
if (content.includes('│Allow') ||
|
|
76
|
+
content.includes('[y/N]') ||
|
|
77
|
+
content.includes('Press any key')) {
|
|
78
|
+
return 'waiting_input';
|
|
79
|
+
}
|
|
80
|
+
// Check for busy state
|
|
81
|
+
if (lowerContent.includes('press esc')) {
|
|
82
|
+
return 'busy';
|
|
83
|
+
}
|
|
84
|
+
// Otherwise idle
|
|
85
|
+
return 'idle';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { ClaudeStateDetector, GeminiStateDetector } from './stateDetector.js';
|
|
2
|
+
import { ClaudeStateDetector, GeminiStateDetector, CodexStateDetector, } from './stateDetector.js';
|
|
3
3
|
describe('ClaudeStateDetector', () => {
|
|
4
4
|
let detector;
|
|
5
5
|
let terminal;
|
|
@@ -240,3 +240,94 @@ describe('GeminiStateDetector', () => {
|
|
|
240
240
|
});
|
|
241
241
|
});
|
|
242
242
|
});
|
|
243
|
+
describe('CodexStateDetector', () => {
|
|
244
|
+
let detector;
|
|
245
|
+
let terminal;
|
|
246
|
+
const createMockTerminal = (lines) => {
|
|
247
|
+
const buffer = {
|
|
248
|
+
length: lines.length,
|
|
249
|
+
active: {
|
|
250
|
+
length: lines.length,
|
|
251
|
+
getLine: (index) => {
|
|
252
|
+
if (index >= 0 && index < lines.length) {
|
|
253
|
+
return {
|
|
254
|
+
translateToString: () => lines[index],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
return { buffer };
|
|
262
|
+
};
|
|
263
|
+
beforeEach(() => {
|
|
264
|
+
detector = new CodexStateDetector();
|
|
265
|
+
});
|
|
266
|
+
it('should detect waiting_input state for │Allow pattern', () => {
|
|
267
|
+
// Arrange
|
|
268
|
+
terminal = createMockTerminal(['Some output', '│Allow execution?', '│ > ']);
|
|
269
|
+
// Act
|
|
270
|
+
const state = detector.detectState(terminal);
|
|
271
|
+
// Assert
|
|
272
|
+
expect(state).toBe('waiting_input');
|
|
273
|
+
});
|
|
274
|
+
it('should detect waiting_input state for [y/N] pattern', () => {
|
|
275
|
+
// Arrange
|
|
276
|
+
terminal = createMockTerminal(['Some output', 'Continue? [y/N]', '> ']);
|
|
277
|
+
// Act
|
|
278
|
+
const state = detector.detectState(terminal);
|
|
279
|
+
// Assert
|
|
280
|
+
expect(state).toBe('waiting_input');
|
|
281
|
+
});
|
|
282
|
+
it('should detect waiting_input state for Press any key pattern', () => {
|
|
283
|
+
// Arrange
|
|
284
|
+
terminal = createMockTerminal([
|
|
285
|
+
'Some output',
|
|
286
|
+
'Press any key to continue...',
|
|
287
|
+
]);
|
|
288
|
+
// Act
|
|
289
|
+
const state = detector.detectState(terminal);
|
|
290
|
+
// Assert
|
|
291
|
+
expect(state).toBe('waiting_input');
|
|
292
|
+
});
|
|
293
|
+
it('should detect busy state for press esc pattern', () => {
|
|
294
|
+
// Arrange
|
|
295
|
+
terminal = createMockTerminal([
|
|
296
|
+
'Processing...',
|
|
297
|
+
'press esc to cancel',
|
|
298
|
+
'Working...',
|
|
299
|
+
]);
|
|
300
|
+
// Act
|
|
301
|
+
const state = detector.detectState(terminal);
|
|
302
|
+
// Assert
|
|
303
|
+
expect(state).toBe('busy');
|
|
304
|
+
});
|
|
305
|
+
it('should detect busy state for PRESS ESC (uppercase)', () => {
|
|
306
|
+
// Arrange
|
|
307
|
+
terminal = createMockTerminal([
|
|
308
|
+
'Processing...',
|
|
309
|
+
'PRESS ESC to stop',
|
|
310
|
+
'Working...',
|
|
311
|
+
]);
|
|
312
|
+
// Act
|
|
313
|
+
const state = detector.detectState(terminal);
|
|
314
|
+
// Assert
|
|
315
|
+
expect(state).toBe('busy');
|
|
316
|
+
});
|
|
317
|
+
it('should detect idle state when no patterns match', () => {
|
|
318
|
+
// Arrange
|
|
319
|
+
terminal = createMockTerminal(['Normal output', 'Some message', 'Ready']);
|
|
320
|
+
// Act
|
|
321
|
+
const state = detector.detectState(terminal);
|
|
322
|
+
// Assert
|
|
323
|
+
expect(state).toBe('idle');
|
|
324
|
+
});
|
|
325
|
+
it('should prioritize waiting_input over busy', () => {
|
|
326
|
+
// Arrange
|
|
327
|
+
terminal = createMockTerminal(['press esc to cancel', '[y/N]']);
|
|
328
|
+
// Act
|
|
329
|
+
const state = detector.detectState(terminal);
|
|
330
|
+
// Assert
|
|
331
|
+
expect(state).toBe('waiting_input');
|
|
332
|
+
});
|
|
333
|
+
});
|
package/dist/types/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type pkg from '@xterm/headless';
|
|
|
3
3
|
import { GitStatus } from '../utils/gitStatus.js';
|
|
4
4
|
export type Terminal = InstanceType<typeof pkg.Terminal>;
|
|
5
5
|
export type SessionState = 'idle' | 'busy' | 'waiting_input';
|
|
6
|
-
export type StateDetectionStrategy = 'claude' | 'gemini';
|
|
6
|
+
export type StateDetectionStrategy = 'claude' | 'gemini' | 'codex';
|
|
7
7
|
export interface Worktree {
|
|
8
8
|
path: string;
|
|
9
9
|
branch?: string;
|