ccmanager 0.0.2 → 0.0.4

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
@@ -43,17 +43,11 @@ export class SessionManager extends EventEmitter {
43
43
  }
44
44
  else if (currentState === 'waiting_input' &&
45
45
  hasBottomBorder &&
46
- !hasWaitingPrompt) {
47
- if (wasWaitingWithBottomBorder) {
48
- // We've already seen the bottom border, transition to idle
49
- newState = 'idle';
50
- this.waitingWithBottomBorder.set(sessionId, false);
51
- }
52
- else {
53
- // First time seeing bottom border, keep waiting state
54
- newState = 'waiting_input';
55
- this.waitingWithBottomBorder.set(sessionId, true);
56
- }
46
+ !hasWaitingPrompt &&
47
+ !wasWaitingWithBottomBorder) {
48
+ // Keep the waiting state and mark that we've seen the bottom border
49
+ newState = 'waiting_input';
50
+ this.waitingWithBottomBorder.set(sessionId, true);
57
51
  // Clear any pending busy timer
58
52
  const existingTimer = this.busyTimers.get(sessionId);
59
53
  if (existingTimer) {
@@ -90,14 +84,6 @@ export class SessionManager extends EventEmitter {
90
84
  // Keep current busy state for now
91
85
  newState = 'busy';
92
86
  }
93
- else if (!hasWaitingPrompt && !hasEscToInterrupt && !hasBottomBorder) {
94
- // No special prompts or indicators, transition to idle
95
- newState = 'idle';
96
- // Clear the waiting flag when transitioning to idle
97
- if (currentState === 'waiting_input') {
98
- this.waitingWithBottomBorder.set(sessionId, false);
99
- }
100
- }
101
87
  return newState;
102
88
  }
103
89
  constructor() {
@@ -131,7 +117,11 @@ export class SessionManager extends EventEmitter {
131
117
  const id = `session-${Date.now()}-${Math.random()
132
118
  .toString(36)
133
119
  .substr(2, 9)}`;
134
- const ptyProcess = spawn('claude', [], {
120
+ // Parse Claude command arguments from environment variable
121
+ const claudeArgs = process.env['CCMANAGER_CLAUDE_ARGS']
122
+ ? process.env['CCMANAGER_CLAUDE_ARGS'].split(' ')
123
+ : [];
124
+ const ptyProcess = spawn('claude', claudeArgs, {
135
125
  name: 'xterm-color',
136
126
  cols: process.stdout.columns || 80,
137
127
  rows: process.stdout.rows || 24,
@@ -62,16 +62,27 @@ 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 = '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
- });
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
+ // });
75
86
  it('should clear waitingWithBottomBorder flag when transitioning to busy', () => {
76
87
  const cleanData = 'Processing... Press ESC to interrupt';
77
88
  const currentState = 'waiting_input';
@@ -84,18 +95,29 @@ describe('SessionManager', () => {
84
95
  const newState = sessionManager.detectSessionState(cleanData, currentState, mockSessionId);
85
96
  expect(newState).toBe('busy');
86
97
  });
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
- });
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
+ // });
99
121
  it('should transition from busy to idle after 500ms timer when no "esc to interrupt"', async () => {
100
122
  // Create a mock session for the timer test
101
123
  const mockWorktreePath = '/test/worktree';
@@ -103,6 +125,7 @@ describe('SessionManager', () => {
103
125
  id: mockSessionId,
104
126
  worktreePath: mockWorktreePath,
105
127
  state: 'busy',
128
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
106
129
  process: {},
107
130
  output: [],
108
131
  outputHistory: [],
@@ -131,6 +154,7 @@ describe('SessionManager', () => {
131
154
  id: mockSessionId,
132
155
  worktreePath: mockWorktreePath,
133
156
  state: 'busy',
157
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
134
158
  process: {},
135
159
  output: [],
136
160
  outputHistory: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
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
- }