ccmanager 0.0.3 → 0.0.5

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
@@ -1,4 +1,4 @@
1
- # CCManager - Claude Code Worktree Manager
1
+ # CCManager - Claude Code Session Manager
2
2
 
3
3
  CCManager is a TUI application for managing multiple Claude Code sessions across Git worktrees.
4
4
 
@@ -48,6 +48,23 @@ $ npm start
48
48
  $ npx ccmanager
49
49
  ```
50
50
 
51
+ ## Environment Variables
52
+
53
+ ### CCMANAGER_CLAUDE_ARGS
54
+
55
+ You can pass additional arguments to Claude Code sessions by setting the `CCMANAGER_CLAUDE_ARGS` environment variable:
56
+
57
+ ```bash
58
+ # Start Claude Code with specific arguments for all sessions
59
+ export CCMANAGER_CLAUDE_ARGS="--resume"
60
+ npx ccmanager
61
+
62
+ # Or set it inline
63
+ CCMANAGER_CLAUDE_ARGS="--resume" npx ccmanager
64
+ ```
65
+
66
+ The arguments are applied to all Claude Code sessions started by CCManager.
67
+
51
68
  ## Keyboard Shortcuts
52
69
 
53
70
  ### Default Shortcuts
@@ -55,6 +55,21 @@ export class SessionManager extends EventEmitter {
55
55
  this.busyTimers.delete(sessionId);
56
56
  }
57
57
  }
58
+ else if (currentState === 'waiting_input' &&
59
+ hasBottomBorder &&
60
+ !hasWaitingPrompt &&
61
+ wasWaitingWithBottomBorder) {
62
+ // We've already seen the bottom border for this waiting prompt,
63
+ // so transition to idle
64
+ newState = 'idle';
65
+ this.waitingWithBottomBorder.set(sessionId, false);
66
+ // Clear any pending busy timer
67
+ const existingTimer = this.busyTimers.get(sessionId);
68
+ if (existingTimer) {
69
+ clearTimeout(existingTimer);
70
+ this.busyTimers.delete(sessionId);
71
+ }
72
+ }
58
73
  else if (hasEscToInterrupt) {
59
74
  // If "esc to interrupt" is present, set state to busy
60
75
  newState = 'busy';
@@ -84,6 +99,18 @@ export class SessionManager extends EventEmitter {
84
99
  // Keep current busy state for now
85
100
  newState = 'busy';
86
101
  }
102
+ else if (currentState === 'waiting_input') {
103
+ // If we're in waiting_input but no special patterns detected,
104
+ // transition to idle and clear the flag
105
+ newState = 'idle';
106
+ this.waitingWithBottomBorder.set(sessionId, false);
107
+ // Clear any pending busy timer
108
+ const existingTimer = this.busyTimers.get(sessionId);
109
+ if (existingTimer) {
110
+ clearTimeout(existingTimer);
111
+ this.busyTimers.delete(sessionId);
112
+ }
113
+ }
87
114
  return newState;
88
115
  }
89
116
  constructor() {
@@ -117,7 +144,11 @@ export class SessionManager extends EventEmitter {
117
144
  const id = `session-${Date.now()}-${Math.random()
118
145
  .toString(36)
119
146
  .substr(2, 9)}`;
120
- const ptyProcess = spawn('claude', [], {
147
+ // Parse Claude command arguments from environment variable
148
+ const claudeArgs = process.env['CCMANAGER_CLAUDE_ARGS']
149
+ ? process.env['CCMANAGER_CLAUDE_ARGS'].split(' ')
150
+ : [];
151
+ const ptyProcess = spawn('claude', claudeArgs, {
121
152
  name: 'xterm-color',
122
153
  cols: process.stdout.columns || 80,
123
154
  rows: process.stdout.rows || 24,
@@ -62,27 +62,16 @@ describe('SessionManager', () => {
62
62
  const newState = sessionManager.detectSessionState(cleanData, currentState, mockSessionId);
63
63
  expect(newState).toBe('busy');
64
64
  });
65
- // it('should not change from waiting_input when bottom border was already seen', () => {
66
- // const cleanData = '└───────────────────────┘';
67
- // const currentState: SessionState = 'waiting_input';
68
- // vi.mocked(includesPromptBoxBottomBorder).mockReturnValue(true);
69
- //
70
- // // First, simulate seeing waiting prompt with bottom border
71
- // sessionManager.detectSessionState(
72
- // '│ Do you want to continue?\n└───────────────────────┘',
73
- // 'idle',
74
- // mockSessionId,
75
- // );
76
- //
77
- // // Now another bottom border appears
78
- // const newState = sessionManager.detectSessionState(
79
- // cleanData,
80
- // currentState,
81
- // mockSessionId,
82
- // );
83
- //
84
- // expect(newState).toBe('idle'); // Should change to idle since we already saw the bottom border
85
- // });
65
+ it('should not change from waiting_input when bottom border was already seen', () => {
66
+ const cleanData = '└───────────────────────┘';
67
+ const currentState = 'waiting_input';
68
+ vi.mocked(includesPromptBoxBottomBorder).mockReturnValue(true);
69
+ // First, simulate seeing waiting prompt with bottom border
70
+ sessionManager.detectSessionState('│ Do you want to continue?\n└───────────────────────┘', 'idle', mockSessionId);
71
+ // Now another bottom border appears
72
+ const newState = sessionManager.detectSessionState(cleanData, currentState, mockSessionId);
73
+ expect(newState).toBe('idle'); // Should change to idle since we already saw the bottom border
74
+ });
86
75
  it('should clear waitingWithBottomBorder flag when transitioning to busy', () => {
87
76
  const cleanData = 'Processing... Press ESC to interrupt';
88
77
  const currentState = 'waiting_input';
@@ -95,29 +84,18 @@ describe('SessionManager', () => {
95
84
  const newState = sessionManager.detectSessionState(cleanData, currentState, mockSessionId);
96
85
  expect(newState).toBe('busy');
97
86
  });
98
- // it('should clear waitingWithBottomBorder flag when transitioning to idle', () => {
99
- // const cleanData = 'Task completed successfully';
100
- // const currentState: SessionState = 'waiting_input';
101
- // vi.mocked(includesPromptBoxBottomBorder).mockReturnValue(false);
102
- //
103
- // // First set up waiting state with bottom border
104
- // vi.mocked(includesPromptBoxBottomBorder).mockReturnValue(true);
105
- // sessionManager.detectSessionState(
106
- // '│ Do you want to continue?\n└───────────────────────┘',
107
- // 'idle',
108
- // mockSessionId,
109
- // );
110
- //
111
- // // Now transition to idle
112
- // vi.mocked(includesPromptBoxBottomBorder).mockReturnValue(false);
113
- // const newState = sessionManager.detectSessionState(
114
- // cleanData,
115
- // currentState,
116
- // mockSessionId,
117
- // );
118
- //
119
- // expect(newState).toBe('idle');
120
- // });
87
+ it('should clear waitingWithBottomBorder flag when transitioning to idle', () => {
88
+ const cleanData = 'Task completed successfully';
89
+ const currentState = 'waiting_input';
90
+ vi.mocked(includesPromptBoxBottomBorder).mockReturnValue(false);
91
+ // First set up waiting state with bottom border
92
+ vi.mocked(includesPromptBoxBottomBorder).mockReturnValue(true);
93
+ sessionManager.detectSessionState('│ Do you want to continue?\n└───────────────────────┘', 'idle', mockSessionId);
94
+ // Now transition to idle
95
+ vi.mocked(includesPromptBoxBottomBorder).mockReturnValue(false);
96
+ const newState = sessionManager.detectSessionState(cleanData, currentState, mockSessionId);
97
+ expect(newState).toBe('idle');
98
+ });
121
99
  it('should transition from busy to idle after 500ms timer when no "esc to interrupt"', async () => {
122
100
  // Create a mock session for the timer test
123
101
  const mockWorktreePath = '/test/worktree';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",
@@ -1,9 +0,0 @@
1
- import React from 'react';
2
- import { SessionState, ViewType, Worktree } from '../types/index.js';
3
- interface StatusBarProps {
4
- view: ViewType;
5
- sessionState?: SessionState;
6
- worktree?: Worktree;
7
- }
8
- export declare const StatusBar: React.FC<StatusBarProps>;
9
- export {};
@@ -1,51 +0,0 @@
1
- import React from 'react';
2
- import { Box, Text } from 'ink';
3
- import { SessionState, ViewType } from '../types/index.js';
4
- export const StatusBar = ({ view, sessionState, worktree, }) => {
5
- const getStateText = () => {
6
- if (!sessionState)
7
- return '';
8
- switch (sessionState) {
9
- case SessionState.Processing:
10
- return 'Processing...';
11
- case SessionState.WaitingForInput:
12
- return 'Ready';
13
- case SessionState.NeedsInteraction:
14
- return 'Needs input';
15
- case SessionState.Error:
16
- return 'Error';
17
- case SessionState.Terminated:
18
- return 'Terminated';
19
- default:
20
- return 'Idle';
21
- }
22
- };
23
- const getStateColor = () => {
24
- if (!sessionState)
25
- return 'gray';
26
- switch (sessionState) {
27
- case SessionState.Processing:
28
- return 'yellow';
29
- case SessionState.WaitingForInput:
30
- return 'green';
31
- case SessionState.NeedsInteraction:
32
- return 'cyan';
33
- case SessionState.Error:
34
- return 'red';
35
- case SessionState.Terminated:
36
- return 'gray';
37
- default:
38
- return 'gray';
39
- }
40
- };
41
- return (React.createElement(Box, { borderStyle: "single", borderTop: true, paddingLeft: 1, paddingRight: 1, justifyContent: "space-between" },
42
- React.createElement(Box, null, worktree && (React.createElement(Text, null,
43
- React.createElement(Text, { bold: true }, worktree.branch),
44
- React.createElement(Text, { dimColor: true },
45
- " (",
46
- worktree.path,
47
- ")")))),
48
- React.createElement(Box, { gap: 2 },
49
- sessionState && React.createElement(Text, { color: getStateColor() }, getStateText()),
50
- React.createElement(Text, { dimColor: true }, view === ViewType.Session ? 'Ctrl+E Return to Menu' : 'Q Quit'))));
51
- };
@@ -1,9 +0,0 @@
1
- import { Session } from '../types/index.js';
2
- export declare const useSession: () => {
3
- sessions: Session[];
4
- createOrActivateSession: (worktreePath: string) => Session;
5
- writeToSession: (sessionId: string, data: string) => void;
6
- resizeSession: (sessionId: string, cols: number, rows: number) => void;
7
- terminateSession: (sessionId: string) => void;
8
- getSession: (id: string) => Session | undefined;
9
- };
@@ -1,44 +0,0 @@
1
- import { useState, useEffect, useCallback } from 'react';
2
- import { SessionManager } from '../services/sessionManager.js';
3
- export const useSession = () => {
4
- const [sessions, setSessions] = useState([]);
5
- const [sessionManager] = useState(() => new SessionManager());
6
- const refreshSessions = useCallback(() => {
7
- setSessions(sessionManager.getAllSessions());
8
- }, [sessionManager]);
9
- const createOrActivateSession = useCallback((worktreePath) => {
10
- let session = sessionManager.findSessionByWorktree(worktreePath);
11
- if (!session) {
12
- session = sessionManager.createSession(worktreePath);
13
- sessionManager.startSession(session.id);
14
- }
15
- else if (!session.process) {
16
- sessionManager.startSession(session.id);
17
- }
18
- refreshSessions();
19
- return session;
20
- }, [sessionManager, refreshSessions]);
21
- const writeToSession = useCallback((sessionId, data) => {
22
- sessionManager.writeToSession(sessionId, data);
23
- }, [sessionManager]);
24
- const resizeSession = useCallback((sessionId, cols, rows) => {
25
- sessionManager.resizeSession(sessionId, cols, rows);
26
- }, [sessionManager]);
27
- const terminateSession = useCallback((sessionId) => {
28
- sessionManager.terminateSession(sessionId);
29
- refreshSessions();
30
- }, [sessionManager, refreshSessions]);
31
- // Update sessions when they change
32
- useEffect(() => {
33
- const interval = setInterval(refreshSessions, 100);
34
- return () => clearInterval(interval);
35
- }, [refreshSessions]);
36
- return {
37
- sessions,
38
- createOrActivateSession,
39
- writeToSession,
40
- resizeSession,
41
- terminateSession,
42
- getSession: (id) => sessionManager.getSession(id),
43
- };
44
- };
@@ -1,7 +0,0 @@
1
- import { Worktree } from '../types/index.js';
2
- export declare const useWorktree: () => {
3
- worktrees: Worktree[];
4
- loading: boolean;
5
- error: string | null;
6
- refreshWorktrees: () => Promise<void>;
7
- };
@@ -1,31 +0,0 @@
1
- import { useState, useEffect } from 'react';
2
- import { WorktreeService } from '../services/worktreeService.js';
3
- export const useWorktree = () => {
4
- const [worktrees, setWorktrees] = useState([]);
5
- const [loading, setLoading] = useState(true);
6
- const [error, setError] = useState(null);
7
- const worktreeService = new WorktreeService();
8
- const refreshWorktrees = async () => {
9
- try {
10
- setLoading(true);
11
- setError(null);
12
- const trees = await worktreeService.listWorktrees();
13
- setWorktrees(trees);
14
- }
15
- catch (err) {
16
- setError(err instanceof Error ? err.message : 'Failed to load worktrees');
17
- }
18
- finally {
19
- setLoading(false);
20
- }
21
- };
22
- useEffect(() => {
23
- refreshWorktrees();
24
- }, []);
25
- return {
26
- worktrees,
27
- loading,
28
- error,
29
- refreshWorktrees,
30
- };
31
- };
package/dist/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/dist/index.js DELETED
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- import React from 'react';
3
- import { render } from 'ink';
4
- import App from './components/App.js';
5
- render(React.createElement(App, null));
@@ -1 +0,0 @@
1
- export declare function log(...args: any[]): void;
@@ -1,10 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- const logFile = path.join(process.cwd(), 'ccmanager.log');
4
- // Clear log file on module load
5
- fs.writeFileSync(logFile, '');
6
- export function log(...args) {
7
- const timestamp = new Date().toISOString();
8
- const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ');
9
- fs.appendFileSync(logFile, `[${timestamp}] ${message}\n`);
10
- }
@@ -1,9 +0,0 @@
1
- import { SessionState } from '../types/index.js';
2
- export declare class StateDetector {
3
- private readonly promptPatterns;
4
- private readonly errorPatterns;
5
- private readonly processingPatterns;
6
- detectState(output: string, currentState: SessionState): SessionState;
7
- private stripAnsi;
8
- private matchesAny;
9
- }
@@ -1,82 +0,0 @@
1
- import { SessionState } from '../types/index.js';
2
- export class StateDetector {
3
- constructor() {
4
- Object.defineProperty(this, "promptPatterns", {
5
- enumerable: true,
6
- configurable: true,
7
- writable: true,
8
- value: [
9
- />\s*$/,
10
- /\?\s*$/,
11
- /:\s*$/,
12
- /Press Enter to continue/i,
13
- /\(y\/n\)/i,
14
- /\[y\/N\]/i,
15
- /Enter your choice/i,
16
- ]
17
- });
18
- Object.defineProperty(this, "errorPatterns", {
19
- enumerable: true,
20
- configurable: true,
21
- writable: true,
22
- value: [
23
- /error:/i,
24
- /failed:/i,
25
- /exception:/i,
26
- /traceback/i,
27
- /command not found/i,
28
- ]
29
- });
30
- Object.defineProperty(this, "processingPatterns", {
31
- enumerable: true,
32
- configurable: true,
33
- writable: true,
34
- value: [
35
- /processing/i,
36
- /loading/i,
37
- /running/i,
38
- /executing/i,
39
- /analyzing/i,
40
- /searching/i,
41
- /\.\.\./,
42
- /in progress/i,
43
- ]
44
- });
45
- }
46
- detectState(output, currentState) {
47
- // Clean ANSI codes for better pattern matching
48
- const cleanOutput = this.stripAnsi(output);
49
- // Check for errors first
50
- if (this.matchesAny(cleanOutput, this.errorPatterns)) {
51
- return SessionState.Error;
52
- }
53
- // Check if processing
54
- if (this.matchesAny(cleanOutput, this.processingPatterns)) {
55
- return SessionState.Processing;
56
- }
57
- // Check for prompts that need interaction
58
- if (this.matchesAny(cleanOutput, this.promptPatterns)) {
59
- // Check if it's a yes/no prompt or choice prompt
60
- if (/\(y\/n\)/i.test(cleanOutput) ||
61
- /\[y\/N\]/i.test(cleanOutput) ||
62
- /Enter your choice/i.test(cleanOutput)) {
63
- return SessionState.NeedsInteraction;
64
- }
65
- return SessionState.WaitingForInput;
66
- }
67
- // If we're getting output but no clear patterns, assume processing
68
- if (cleanOutput.trim().length > 0 &&
69
- currentState === SessionState.WaitingForInput) {
70
- return SessionState.Processing;
71
- }
72
- // Default to current state
73
- return currentState;
74
- }
75
- stripAnsi(str) {
76
- // eslint-disable-next-line no-control-regex
77
- return str.replace(/\x1b\[[0-9;]*m/g, '');
78
- }
79
- matchesAny(text, patterns) {
80
- return patterns.some(pattern => pattern.test(text));
81
- }
82
- }
@@ -1 +0,0 @@
1
- export declare function debugLog(location: string, message: string, data?: any): void;
@@ -1,109 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { homedir } from 'os';
4
- class DebugLogger {
5
- constructor() {
6
- Object.defineProperty(this, "logFile", {
7
- enumerable: true,
8
- configurable: true,
9
- writable: true,
10
- value: void 0
11
- });
12
- Object.defineProperty(this, "stream", {
13
- enumerable: true,
14
- configurable: true,
15
- writable: true,
16
- value: null
17
- });
18
- // Create log file in user's home directory
19
- const logDir = path.join(homedir(), '.ccmanager');
20
- if (!fs.existsSync(logDir)) {
21
- fs.mkdirSync(logDir, { recursive: true });
22
- }
23
- this.logFile = path.join(logDir, 'debug.log');
24
- this.initStream();
25
- }
26
- initStream() {
27
- this.stream = fs.createWriteStream(this.logFile, { flags: 'a' });
28
- // Add separator when starting new session
29
- this.stream.write('\n' + '='.repeat(80) + '\n');
30
- this.stream.write(`Session started at ${new Date().toISOString()}\n`);
31
- this.stream.write('='.repeat(80) + '\n\n');
32
- }
33
- stripAnsi(str) {
34
- // Remove ANSI escape sequences
35
- return str.replace(/\x1B\[[0-9;]*m/g, '') // Color codes
36
- .replace(/\x1B\[\?[0-9]+[hl]/g, '') // Cursor visibility
37
- .replace(/\x1B\[[0-9]*[ABCDEFGHJKST]/g, '') // Cursor movement
38
- .replace(/\x1B\[[0-9]*K/g, '') // Clear line
39
- .replace(/\r/g, '') // Carriage returns
40
- .replace(/\x1B\[2004[hl]/g, '') // Bracketed paste mode
41
- .replace(/\x1B\[1004[hl]/g, ''); // Focus tracking mode
42
- }
43
- formatData(data) {
44
- if (typeof data === 'string') {
45
- const cleaned = this.stripAnsi(data);
46
- // Only show non-empty, meaningful content
47
- if (cleaned.trim()) {
48
- // Truncate very long outputs
49
- if (cleaned.length > 200) {
50
- return cleaned.substring(0, 200) + '... [truncated]';
51
- }
52
- return cleaned;
53
- }
54
- return '[empty or control sequences only]';
55
- }
56
- else if (typeof data === 'object' && data !== null) {
57
- // Pretty print objects
58
- return JSON.stringify(data, null, 2);
59
- }
60
- return String(data);
61
- }
62
- log(location, message, data) {
63
- if (!this.stream)
64
- return;
65
- const timestampParts = new Date().toISOString().split('T');
66
- const timestamp = timestampParts[1] ? timestampParts[1].replace('Z', '') : new Date().toTimeString().split(' ')[0];
67
- // Format the log entry in a human-readable way
68
- this.stream.write(`[${timestamp}] ${location}\n`);
69
- this.stream.write(` ${message}:\n`);
70
- if (data !== undefined) {
71
- const formattedData = this.formatData(data);
72
- const lines = formattedData.split('\n');
73
- lines.forEach(line => {
74
- if (this.stream) {
75
- this.stream.write(` ${line}\n`);
76
- }
77
- });
78
- }
79
- if (this.stream) {
80
- this.stream.write('\n');
81
- }
82
- }
83
- close() {
84
- if (this.stream) {
85
- this.stream.write(`\nSession ended at ${new Date().toISOString()}\n`);
86
- this.stream.write('='.repeat(80) + '\n');
87
- this.stream.end();
88
- this.stream = null;
89
- }
90
- }
91
- }
92
- // Singleton instance
93
- const debugLogger = new DebugLogger();
94
- // Export a simple function for logging
95
- export function debugLog(location, message, data) {
96
- debugLogger.log(location, message, data);
97
- }
98
- // Clean up on process exit
99
- process.on('exit', () => {
100
- debugLogger.close();
101
- });
102
- process.on('SIGINT', () => {
103
- debugLogger.close();
104
- process.exit();
105
- });
106
- process.on('SIGTERM', () => {
107
- debugLogger.close();
108
- process.exit();
109
- });
@@ -1 +0,0 @@
1
- export declare function isWaitingForInput(output: string): boolean;
@@ -1,16 +0,0 @@
1
- export function isWaitingForInput(output) {
2
- // Don't trim - we need to check end patterns
3
- if (!output) {
4
- return false;
5
- }
6
- // Check if output ends with Claude prompt ("> " at the end)
7
- if (output.trimEnd().endsWith('>')) {
8
- return true;
9
- }
10
- // Check for user interaction
11
- // │ Do you want to proceed?
12
- if (output.includes('│ Do you want')) {
13
- return true;
14
- }
15
- return false;
16
- }