ccmanager 4.0.1 → 4.0.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/dist/components/ConfigureStatusHooks.js +1 -1
- package/dist/components/Session.js +46 -63
- package/dist/services/sessionManager.js +3 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/hookExecutor.js +3 -2
- package/dist/utils/hookExecutor.test.js +4 -0
- package/dist/utils/worktreeUtils.test.js +1 -0
- package/package.json +6 -6
|
@@ -105,7 +105,7 @@ const ConfigureStatusHooks = ({ onComplete, }) => {
|
|
|
105
105
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: "green", children: "\u2713 Configuration saved successfully!" }) }));
|
|
106
106
|
}
|
|
107
107
|
if (view === 'edit') {
|
|
108
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "green", children: ["Configure ", STATUS_LABELS[selectedStatus], " Hook"] }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Command to execute when status changes to", ' ', STATUS_LABELS[selectedStatus], ":"] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(TextInputWrapper, { value: currentCommand, onChange: setCurrentCommand, onSubmit: handleCommandSubmit, placeholder: "Enter command (e.g., notify-send 'Claude is idle')" }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Enabled: ", currentEnabled ? '✓' : '✗', " (Press Tab to toggle)"] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Environment variables available: CCMANAGER_OLD_STATE, CCMANAGER_NEW_STATE," }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `CCMANAGER_WORKTREE_PATH, CCMANAGER_WORKTREE_DIR, CCMANAGER_WORKTREE_BRANCH, CCMANAGER_SESSION_ID` }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to save, Tab to toggle enabled, Esc to cancel" }) })] }));
|
|
108
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "green", children: ["Configure ", STATUS_LABELS[selectedStatus], " Hook"] }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Command to execute when status changes to", ' ', STATUS_LABELS[selectedStatus], ":"] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(TextInputWrapper, { value: currentCommand, onChange: setCurrentCommand, onSubmit: handleCommandSubmit, placeholder: "Enter command (e.g., notify-send 'Claude is idle')" }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Enabled: ", currentEnabled ? '✓' : '✗', " (Press Tab to toggle)"] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Environment variables available: CCMANAGER_OLD_STATE, CCMANAGER_NEW_STATE," }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `CCMANAGER_WORKTREE_PATH, CCMANAGER_WORKTREE_DIR, CCMANAGER_WORKTREE_BRANCH, CCMANAGER_SESSION_ID, CCMANAGER_PRESET_NAME` }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to save, Tab to toggle enabled, Esc to cancel" }) })] }));
|
|
109
109
|
}
|
|
110
110
|
const scopeLabel = scope === 'project' ? 'Project' : 'Global';
|
|
111
111
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "green", children: ["Configure Status Hooks (", scopeLabel, ")"] }) }), isInheriting && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { backgroundColor: "cyan", color: "black", children: [' ', "\uD83D\uDCCB Inheriting from global configuration", ' '] }) })), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Set commands to run when session status changes:" }) }), _jsx(SelectInput, { items: getMenuItems(), onSelect: handleMenuSelect, isFocused: true, limit: 10 }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Esc to go back" }) })] }));
|
|
@@ -4,11 +4,6 @@ import { shortcutManager } from '../services/shortcutManager.js';
|
|
|
4
4
|
const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
5
5
|
const { stdout } = useStdout();
|
|
6
6
|
const isExitingRef = useRef(false);
|
|
7
|
-
const stripOscColorSequences = (input) => {
|
|
8
|
-
// Remove default foreground/background color OSC sequences that Codex emits
|
|
9
|
-
// These sequences leak as literal text when replaying buffered output
|
|
10
|
-
return input.replace(/\x1B\](?:10|11);[^\x07\x1B]*(?:\x07|\x1B\\)/g, '');
|
|
11
|
-
};
|
|
12
7
|
const normalizeLineEndings = (input) => {
|
|
13
8
|
// Ensure LF moves to column 0 to prevent cursor drift when ONLCR is disabled.
|
|
14
9
|
let normalized = '';
|
|
@@ -32,22 +27,40 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
32
27
|
// protocol / modifyOtherKeys / focus tracking) so they don't leak into other
|
|
33
28
|
// sessions after we detach.
|
|
34
29
|
stdout.write('\x1b[>0u'); // Disable kitty keyboard protocol (CSI u sequences)
|
|
35
|
-
stdout.write('\x1b[>
|
|
30
|
+
stdout.write('\x1b[>4;0m'); // Disable xterm modifyOtherKeys extensions
|
|
36
31
|
stdout.write('\x1b[?1004l'); // Disable focus reporting
|
|
37
32
|
stdout.write('\x1b[?2004l'); // Disable bracketed paste (can interfere with shortcuts)
|
|
38
33
|
stdout.write('\x1b[?7h'); // Re-enable auto-wrap
|
|
39
34
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
35
|
+
// Set up raw input handling
|
|
36
|
+
const stdin = process.stdin;
|
|
37
|
+
// Configure stdin for PTY passthrough
|
|
38
|
+
if (stdin.isTTY) {
|
|
39
|
+
stdin.setRawMode(true);
|
|
40
|
+
stdin.resume();
|
|
41
|
+
}
|
|
42
|
+
stdin.setEncoding('utf8');
|
|
43
|
+
const handleStdinData = (data) => {
|
|
44
|
+
if (isExitingRef.current)
|
|
45
|
+
return;
|
|
46
|
+
// Check for return to menu shortcut
|
|
47
|
+
if (shortcutManager.matchesRawInput('returnToMenu', data)) {
|
|
48
|
+
// Disable any extended input modes that might have been enabled by the PTY
|
|
49
|
+
if (stdout) {
|
|
50
|
+
resetTerminalInputModes();
|
|
51
|
+
}
|
|
52
|
+
// Remove our listener — Ink will reconfigure stdin when Menu mounts
|
|
53
|
+
stdin.removeListener('data', handleStdinData);
|
|
54
|
+
onReturnToMenu();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (session.stateMutex.getSnapshot().state === 'pending_auto_approval') {
|
|
58
|
+
sessionManager.cancelAutoApproval(session.id, 'User input received during auto-approval');
|
|
59
|
+
}
|
|
60
|
+
// Pass all other input directly to the PTY
|
|
61
|
+
session.process.write(data);
|
|
48
62
|
};
|
|
49
|
-
|
|
50
|
-
resetTerminalInputModes();
|
|
63
|
+
stdin.on('data', handleStdinData);
|
|
51
64
|
// Prevent line wrapping from drifting redraws in TUIs that rely on cursor-up clears.
|
|
52
65
|
stdout.write('\x1b[?7l');
|
|
53
66
|
// Clear screen when entering session
|
|
@@ -61,9 +74,8 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
61
74
|
// Concatenate all history buffers and write at once for better performance
|
|
62
75
|
const allHistory = Buffer.concat(restoredSession.outputHistory);
|
|
63
76
|
const historyStr = allHistory.toString('utf8');
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
const normalized = normalizeLineEndings(sanitized);
|
|
77
|
+
// Normalize the output
|
|
78
|
+
const normalized = normalizeLineEndings(historyStr);
|
|
67
79
|
// Remove leading clear screen sequences to avoid double-clear
|
|
68
80
|
const cleaned = normalized
|
|
69
81
|
.replace(/^\x1B\[2J/g, '')
|
|
@@ -75,6 +87,21 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
75
87
|
};
|
|
76
88
|
// Listen for restore event first
|
|
77
89
|
sessionManager.on('sessionRestore', handleSessionRestore);
|
|
90
|
+
// Listen for session data events
|
|
91
|
+
const handleSessionData = (activeSession, data) => {
|
|
92
|
+
// Only handle data for our session
|
|
93
|
+
if (activeSession.id === session.id && !isExitingRef.current) {
|
|
94
|
+
stdout.write(normalizeLineEndings(data));
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
const handleSessionExit = (exitedSession) => {
|
|
98
|
+
if (exitedSession.id === session.id) {
|
|
99
|
+
isExitingRef.current = true;
|
|
100
|
+
// Don't call onReturnToMenu here - App component handles it
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
sessionManager.on('sessionData', handleSessionData);
|
|
104
|
+
sessionManager.on('sessionExit', handleSessionExit);
|
|
78
105
|
// Mark session as active (this will trigger the restore event)
|
|
79
106
|
sessionManager.setSessionActive(session.id, true);
|
|
80
107
|
// Immediately resize the PTY and terminal to current dimensions
|
|
@@ -93,21 +120,6 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
93
120
|
catch {
|
|
94
121
|
/* empty */
|
|
95
122
|
}
|
|
96
|
-
// Listen for session data events
|
|
97
|
-
const handleSessionData = (activeSession, data) => {
|
|
98
|
-
// Only handle data for our session
|
|
99
|
-
if (activeSession.id === session.id && !isExitingRef.current) {
|
|
100
|
-
stdout.write(normalizeLineEndings(data));
|
|
101
|
-
}
|
|
102
|
-
};
|
|
103
|
-
const handleSessionExit = (exitedSession) => {
|
|
104
|
-
if (exitedSession.id === session.id) {
|
|
105
|
-
isExitingRef.current = true;
|
|
106
|
-
// Don't call onReturnToMenu here - App component handles it
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
sessionManager.on('sessionData', handleSessionData);
|
|
110
|
-
sessionManager.on('sessionExit', handleSessionExit);
|
|
111
123
|
// Handle terminal resize
|
|
112
124
|
const handleResize = () => {
|
|
113
125
|
const cols = process.stdout.columns || 80;
|
|
@@ -119,35 +131,6 @@ const Session = ({ session, sessionManager, onReturnToMenu, }) => {
|
|
|
119
131
|
}
|
|
120
132
|
};
|
|
121
133
|
stdout.on('resize', handleResize);
|
|
122
|
-
// Set up raw input handling
|
|
123
|
-
const stdin = process.stdin;
|
|
124
|
-
// Configure stdin for PTY passthrough
|
|
125
|
-
if (stdin.isTTY) {
|
|
126
|
-
stdin.setRawMode(true);
|
|
127
|
-
stdin.resume();
|
|
128
|
-
}
|
|
129
|
-
stdin.setEncoding('utf8');
|
|
130
|
-
const handleStdinData = (data) => {
|
|
131
|
-
if (isExitingRef.current)
|
|
132
|
-
return;
|
|
133
|
-
// Check for return to menu shortcut
|
|
134
|
-
if (shortcutManager.matchesRawInput('returnToMenu', data)) {
|
|
135
|
-
// Disable any extended input modes that might have been enabled by the PTY
|
|
136
|
-
if (stdout) {
|
|
137
|
-
resetTerminalInputModes();
|
|
138
|
-
}
|
|
139
|
-
// Remove our listener — Ink will reconfigure stdin when Menu mounts
|
|
140
|
-
stdin.removeListener('data', handleStdinData);
|
|
141
|
-
onReturnToMenu();
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
if (session.stateMutex.getSnapshot().state === 'pending_auto_approval') {
|
|
145
|
-
sessionManager.cancelAutoApproval(session.id, 'User input received during auto-approval');
|
|
146
|
-
}
|
|
147
|
-
// Pass all other input directly to the PTY
|
|
148
|
-
session.process.write(data);
|
|
149
|
-
};
|
|
150
|
-
stdin.on('data', handleStdinData);
|
|
151
134
|
return () => {
|
|
152
135
|
// Remove our stdin listener
|
|
153
136
|
stdin.removeListener('data', handleStdinData);
|
|
@@ -217,6 +217,7 @@ export class SessionManager extends EventEmitter {
|
|
|
217
217
|
terminal,
|
|
218
218
|
stateCheckInterval: undefined, // Will be set in setupBackgroundHandler
|
|
219
219
|
isPrimaryCommand: options.isPrimaryCommand ?? true,
|
|
220
|
+
presetName: options.presetName,
|
|
220
221
|
detectionStrategy,
|
|
221
222
|
devcontainerConfig: options.devcontainerConfig ?? undefined,
|
|
222
223
|
stateMutex: new Mutex(createInitialSessionStateData()),
|
|
@@ -257,6 +258,7 @@ export class SessionManager extends EventEmitter {
|
|
|
257
258
|
const ptyProcess = await this.spawn(command, args, worktreePath);
|
|
258
259
|
const session = await this.createSessionInternal(worktreePath, ptyProcess, {
|
|
259
260
|
isPrimaryCommand: true,
|
|
261
|
+
presetName: preset.name,
|
|
260
262
|
detectionStrategy: preset.detectionStrategy,
|
|
261
263
|
});
|
|
262
264
|
if (launch.stdinPayload) {
|
|
@@ -659,6 +661,7 @@ export class SessionManager extends EventEmitter {
|
|
|
659
661
|
const ptyProcess = await this.spawn(devcontainerCmd, fullArgs, worktreePath);
|
|
660
662
|
const session = await this.createSessionInternal(worktreePath, ptyProcess, {
|
|
661
663
|
isPrimaryCommand: true,
|
|
664
|
+
presetName: preset.name,
|
|
662
665
|
detectionStrategy: preset.detectionStrategy,
|
|
663
666
|
devcontainerConfig,
|
|
664
667
|
});
|
package/dist/types/index.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export interface Session {
|
|
|
29
29
|
terminal: Terminal;
|
|
30
30
|
stateCheckInterval: NodeJS.Timeout | undefined;
|
|
31
31
|
isPrimaryCommand: boolean;
|
|
32
|
+
presetName: string | undefined;
|
|
32
33
|
detectionStrategy: StateDetectionStrategy | undefined;
|
|
33
34
|
devcontainerConfig: DevcontainerConfig | undefined;
|
|
34
35
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { basename } from 'path';
|
|
3
3
|
import { Effect } from 'effect';
|
|
4
4
|
import { ProcessError } from '../types/errors.js';
|
|
5
5
|
import { WorktreeService } from '../services/worktreeService.js';
|
|
@@ -149,12 +149,13 @@ export function executeStatusHook(oldState, newState, session) {
|
|
|
149
149
|
// Build environment for status hook
|
|
150
150
|
const environment = {
|
|
151
151
|
CCMANAGER_WORKTREE_PATH: session.worktreePath,
|
|
152
|
-
CCMANAGER_WORKTREE_DIR:
|
|
152
|
+
CCMANAGER_WORKTREE_DIR: basename(session.worktreePath),
|
|
153
153
|
CCMANAGER_WORKTREE_BRANCH: branch,
|
|
154
154
|
CCMANAGER_GIT_ROOT: session.worktreePath, // For status hooks, we use worktree path as cwd
|
|
155
155
|
CCMANAGER_OLD_STATE: oldState,
|
|
156
156
|
CCMANAGER_NEW_STATE: newState,
|
|
157
157
|
CCMANAGER_SESSION_ID: session.id,
|
|
158
|
+
CCMANAGER_PRESET_NAME: session.presetName || '',
|
|
158
159
|
};
|
|
159
160
|
yield* Effect.catchAll(executeHook(hook.command, session.worktreePath, environment), error => {
|
|
160
161
|
// Log error but don't throw - hooks should not break the main flow
|
|
@@ -384,6 +384,7 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
384
384
|
outputHistory: [],
|
|
385
385
|
stateCheckInterval: undefined,
|
|
386
386
|
isPrimaryCommand: true,
|
|
387
|
+
presetName: undefined,
|
|
387
388
|
detectionStrategy: 'claude',
|
|
388
389
|
devcontainerConfig: undefined,
|
|
389
390
|
lastActivity: new Date(),
|
|
@@ -440,6 +441,7 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
440
441
|
outputHistory: [],
|
|
441
442
|
stateCheckInterval: undefined,
|
|
442
443
|
isPrimaryCommand: true,
|
|
444
|
+
presetName: undefined,
|
|
443
445
|
detectionStrategy: 'claude',
|
|
444
446
|
devcontainerConfig: undefined,
|
|
445
447
|
lastActivity: new Date(),
|
|
@@ -494,6 +496,7 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
494
496
|
outputHistory: [],
|
|
495
497
|
stateCheckInterval: undefined,
|
|
496
498
|
isPrimaryCommand: true,
|
|
499
|
+
presetName: undefined,
|
|
497
500
|
detectionStrategy: 'claude',
|
|
498
501
|
devcontainerConfig: undefined,
|
|
499
502
|
lastActivity: new Date(),
|
|
@@ -550,6 +553,7 @@ describe('hookExecutor Integration Tests', () => {
|
|
|
550
553
|
outputHistory: [],
|
|
551
554
|
stateCheckInterval: undefined,
|
|
552
555
|
isPrimaryCommand: true,
|
|
556
|
+
presetName: undefined,
|
|
553
557
|
detectionStrategy: 'claude',
|
|
554
558
|
devcontainerConfig: undefined,
|
|
555
559
|
lastActivity: new Date(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kodai Kabasawa",
|
|
@@ -41,11 +41,11 @@
|
|
|
41
41
|
"bin"
|
|
42
42
|
],
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@kodaikabasawa/ccmanager-darwin-arm64": "4.0.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "4.0.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "4.0.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "4.0.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "4.0.
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "4.0.2",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "4.0.2",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "4.0.2",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "4.0.2",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "4.0.2"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|