ccmanager 1.3.1 → 1.4.0
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 +36 -0
- package/dist/components/App.js +2 -2
- package/dist/components/ConfigureWorktree.js +10 -1
- package/dist/components/NewWorktree.d.ts +1 -1
- package/dist/components/NewWorktree.js +20 -17
- package/dist/services/configurationManager.js +4 -0
- package/dist/services/worktreeService.d.ts +2 -1
- package/dist/services/worktreeService.js +32 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/claudeDir.d.ts +22 -0
- package/dist/utils/claudeDir.js +39 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ https://github.com/user-attachments/assets/15914a88-e288-4ac9-94d5-8127f2e19dbf
|
|
|
15
15
|
- Switch between sessions seamlessly
|
|
16
16
|
- Visual status indicators for session states (busy, waiting, idle)
|
|
17
17
|
- Create, merge, and delete worktrees from within the app
|
|
18
|
+
- **Copy Claude Code session data** between worktrees to maintain conversation context
|
|
18
19
|
- Configurable keyboard shortcuts
|
|
19
20
|
- Command presets with automatic fallback support
|
|
20
21
|
- Configurable state detection strategies for different CLI tools
|
|
@@ -154,6 +155,41 @@ CCManager supports configuring the command and arguments used to run Claude Code
|
|
|
154
155
|
For detailed configuration options and examples, see [docs/command-config.md](docs/command-config.md).
|
|
155
156
|
|
|
156
157
|
|
|
158
|
+
## Session Data Copying
|
|
159
|
+
|
|
160
|
+
CCManager can copy Claude Code session data (conversation history, context, and project state) when creating new worktrees, allowing you to maintain context across different branches.
|
|
161
|
+
|
|
162
|
+
### Features
|
|
163
|
+
|
|
164
|
+
- **Seamless Context Transfer**: Continue conversations in new worktrees without losing context
|
|
165
|
+
- **Configurable Default**: Set whether to copy session data by default
|
|
166
|
+
- **Per-Creation Choice**: Decide on each worktree creation whether to copy data
|
|
167
|
+
- **Safe Operation**: Copying is non-fatal - worktree creation succeeds even if copying fails
|
|
168
|
+
|
|
169
|
+
### How It Works
|
|
170
|
+
|
|
171
|
+
When creating a new worktree, CCManager:
|
|
172
|
+
1. Asks whether to copy session data from the current worktree
|
|
173
|
+
2. Copies all session files from `~/.claude/projects/[source-path]` to `~/.claude/projects/[target-path]`
|
|
174
|
+
3. Preserves conversation history, project context, and Claude Code state
|
|
175
|
+
4. Allows immediate continuation of conversations in the new worktree
|
|
176
|
+
|
|
177
|
+
### Configuration
|
|
178
|
+
|
|
179
|
+
1. Navigate to **Configuration** → **Configure Worktree**
|
|
180
|
+
2. Toggle **Copy Session Data** to set the default behavior
|
|
181
|
+
3. Save changes
|
|
182
|
+
|
|
183
|
+
The default choice (copy or start fresh) will be pre-selected when creating new worktrees.
|
|
184
|
+
|
|
185
|
+
### Use Cases
|
|
186
|
+
|
|
187
|
+
- **Feature Development**: Copy session data when creating feature branches to maintain project context
|
|
188
|
+
- **Experimentation**: Start fresh when testing unrelated changes
|
|
189
|
+
- **Collaboration**: Share session state across team worktrees
|
|
190
|
+
- **Context Preservation**: Maintain long conversations across multiple development branches
|
|
191
|
+
|
|
192
|
+
|
|
157
193
|
## Status Change Hooks
|
|
158
194
|
|
|
159
195
|
CCManager can execute custom commands when Claude Code session status changes. This enables powerful automation workflows like desktop notifications, logging, or integration with other tools.
|
package/dist/components/App.js
CHANGED
|
@@ -150,11 +150,11 @@ const App = ({ devcontainerConfig }) => {
|
|
|
150
150
|
}
|
|
151
151
|
}, 50); // Small delay to ensure proper cleanup
|
|
152
152
|
};
|
|
153
|
-
const handleCreateWorktree = async (path, branch, baseBranch, copyClaudeDirectory) => {
|
|
153
|
+
const handleCreateWorktree = async (path, branch, baseBranch, copySessionData, copyClaudeDirectory) => {
|
|
154
154
|
setView('creating-worktree');
|
|
155
155
|
setError(null);
|
|
156
156
|
// Create the worktree
|
|
157
|
-
const result = worktreeService.createWorktree(path, branch, baseBranch, copyClaudeDirectory);
|
|
157
|
+
const result = worktreeService.createWorktree(path, branch, baseBranch, copySessionData, copyClaudeDirectory);
|
|
158
158
|
if (result.success) {
|
|
159
159
|
// Success - return to menu
|
|
160
160
|
handleReturnToMenu();
|
|
@@ -8,6 +8,7 @@ const ConfigureWorktree = ({ onComplete }) => {
|
|
|
8
8
|
const worktreeConfig = configurationManager.getWorktreeConfig();
|
|
9
9
|
const [autoDirectory, setAutoDirectory] = useState(worktreeConfig.autoDirectory);
|
|
10
10
|
const [pattern, setPattern] = useState(worktreeConfig.autoDirectoryPattern || '../{branch}');
|
|
11
|
+
const [copySessionData, setCopySessionData] = useState(worktreeConfig.copySessionData ?? true);
|
|
11
12
|
const [editMode, setEditMode] = useState('menu');
|
|
12
13
|
const [tempPattern, setTempPattern] = useState(pattern);
|
|
13
14
|
useInput((input, key) => {
|
|
@@ -25,6 +26,10 @@ const ConfigureWorktree = ({ onComplete }) => {
|
|
|
25
26
|
label: `Pattern: ${pattern}`,
|
|
26
27
|
value: 'pattern',
|
|
27
28
|
},
|
|
29
|
+
{
|
|
30
|
+
label: `Copy Session Data: ${copySessionData ? '✅ Enabled' : '❌ Disabled'}`,
|
|
31
|
+
value: 'toggleCopy',
|
|
32
|
+
},
|
|
28
33
|
{
|
|
29
34
|
label: '💾 Save Changes',
|
|
30
35
|
value: 'save',
|
|
@@ -43,11 +48,15 @@ const ConfigureWorktree = ({ onComplete }) => {
|
|
|
43
48
|
setTempPattern(pattern);
|
|
44
49
|
setEditMode('pattern');
|
|
45
50
|
break;
|
|
51
|
+
case 'toggleCopy':
|
|
52
|
+
setCopySessionData(!copySessionData);
|
|
53
|
+
break;
|
|
46
54
|
case 'save':
|
|
47
55
|
// Save the configuration
|
|
48
56
|
configurationManager.setWorktreeConfig({
|
|
49
57
|
autoDirectory,
|
|
50
58
|
autoDirectoryPattern: pattern,
|
|
59
|
+
copySessionData,
|
|
51
60
|
});
|
|
52
61
|
onComplete();
|
|
53
62
|
break;
|
|
@@ -83,7 +92,7 @@ const ConfigureWorktree = ({ onComplete }) => {
|
|
|
83
92
|
React.createElement(Box, { marginBottom: 1 },
|
|
84
93
|
React.createElement(Text, { bold: true, color: "green" }, "Configure Worktree Settings")),
|
|
85
94
|
React.createElement(Box, { marginBottom: 1 },
|
|
86
|
-
React.createElement(Text, { dimColor: true }, "Configure
|
|
95
|
+
React.createElement(Text, { dimColor: true }, "Configure worktree creation settings")),
|
|
87
96
|
autoDirectory && (React.createElement(Box, { marginBottom: 1 },
|
|
88
97
|
React.createElement(Text, null,
|
|
89
98
|
"Example: branch \"feature/my-feature\" \u2192 directory \"",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
interface NewWorktreeProps {
|
|
3
|
-
onComplete: (path: string, branch: string, baseBranch: string, copyClaudeDirectory: boolean) => void;
|
|
3
|
+
onComplete: (path: string, branch: string, baseBranch: string, copySessionData: boolean, copyClaudeDirectory: boolean) => void;
|
|
4
4
|
onCancel: () => void;
|
|
5
5
|
}
|
|
6
6
|
declare const NewWorktree: React.FC<NewWorktreeProps>;
|
|
@@ -14,6 +14,8 @@ const NewWorktree = ({ onComplete, onCancel }) => {
|
|
|
14
14
|
const [path, setPath] = useState('');
|
|
15
15
|
const [branch, setBranch] = useState('');
|
|
16
16
|
const [baseBranch, setBaseBranch] = useState('');
|
|
17
|
+
const [copyClaudeDirectory, setCopyClaudeDirectory] = useState(true);
|
|
18
|
+
const [copySessionData, setCopySessionData] = useState(worktreeConfig.copySessionData ?? true);
|
|
17
19
|
// Initialize worktree service and load branches (memoized to avoid re-initialization)
|
|
18
20
|
const { branches, defaultBranch } = useMemo(() => {
|
|
19
21
|
const service = new WorktreeService();
|
|
@@ -50,30 +52,22 @@ const NewWorktree = ({ onComplete, onCancel }) => {
|
|
|
50
52
|
};
|
|
51
53
|
const handleBaseBranchSelect = (item) => {
|
|
52
54
|
setBaseBranch(item.value);
|
|
53
|
-
|
|
54
|
-
const service = new WorktreeService();
|
|
55
|
-
if (service.hasClaudeDirectoryInBranch(item.value)) {
|
|
56
|
-
setStep('copy-settings');
|
|
57
|
-
}
|
|
58
|
-
else {
|
|
59
|
-
// Skip copy-settings step and complete with copySettings = false
|
|
60
|
-
if (isAutoDirectory) {
|
|
61
|
-
const autoPath = generateWorktreeDirectory(branch, worktreeConfig.autoDirectoryPattern);
|
|
62
|
-
onComplete(autoPath, branch, item.value, false);
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
onComplete(path, branch, item.value, false);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
55
|
+
setStep('copy-settings');
|
|
68
56
|
};
|
|
69
57
|
const handleCopySettingsSelect = (item) => {
|
|
58
|
+
setCopyClaudeDirectory(item.value);
|
|
59
|
+
setStep('copy-session');
|
|
60
|
+
};
|
|
61
|
+
const handleCopySessionSelect = (item) => {
|
|
62
|
+
const shouldCopy = item.value === 'yes';
|
|
63
|
+
setCopySessionData(shouldCopy);
|
|
70
64
|
if (isAutoDirectory) {
|
|
71
65
|
// Generate path from branch name
|
|
72
66
|
const autoPath = generateWorktreeDirectory(branch, worktreeConfig.autoDirectoryPattern);
|
|
73
|
-
onComplete(autoPath, branch, baseBranch,
|
|
67
|
+
onComplete(autoPath, branch, baseBranch, shouldCopy, copyClaudeDirectory);
|
|
74
68
|
}
|
|
75
69
|
else {
|
|
76
|
-
onComplete(path, branch, baseBranch,
|
|
70
|
+
onComplete(path, branch, baseBranch, shouldCopy, copyClaudeDirectory);
|
|
77
71
|
}
|
|
78
72
|
};
|
|
79
73
|
// Calculate generated path for preview (memoized to avoid expensive recalculations)
|
|
@@ -129,6 +123,15 @@ const NewWorktree = ({ onComplete, onCancel }) => {
|
|
|
129
123
|
},
|
|
130
124
|
{ label: 'No - Start without .claude directory', value: false },
|
|
131
125
|
], onSelect: handleCopySettingsSelect, initialIndex: 0 }))),
|
|
126
|
+
step === 'copy-session' && (React.createElement(Box, { flexDirection: "column" },
|
|
127
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
128
|
+
React.createElement(Text, null, "Copy Claude Code session data to the new worktree?")),
|
|
129
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
130
|
+
React.createElement(Text, { dimColor: true }, "This will copy conversation history and context from the current worktree")),
|
|
131
|
+
React.createElement(SelectInput, { items: [
|
|
132
|
+
{ label: '✅ Yes, copy session data', value: 'yes' },
|
|
133
|
+
{ label: '❌ No, start fresh', value: 'no' },
|
|
134
|
+
], onSelect: handleCopySessionSelect, initialIndex: copySessionData ? 0 : 1 }))),
|
|
132
135
|
React.createElement(Box, { marginTop: 1 },
|
|
133
136
|
React.createElement(Text, { dimColor: true },
|
|
134
137
|
"Press ",
|
|
@@ -66,8 +66,12 @@ export class ConfigurationManager {
|
|
|
66
66
|
if (!this.config.worktree) {
|
|
67
67
|
this.config.worktree = {
|
|
68
68
|
autoDirectory: false,
|
|
69
|
+
copySessionData: true,
|
|
69
70
|
};
|
|
70
71
|
}
|
|
72
|
+
if (!Object.prototype.hasOwnProperty.call(this.config.worktree, 'copySessionData')) {
|
|
73
|
+
this.config.worktree.copySessionData = true;
|
|
74
|
+
}
|
|
71
75
|
if (!this.config.command) {
|
|
72
76
|
this.config.command = {
|
|
73
77
|
command: 'claude',
|
|
@@ -9,7 +9,7 @@ export declare class WorktreeService {
|
|
|
9
9
|
isGitRepository(): boolean;
|
|
10
10
|
getDefaultBranch(): string;
|
|
11
11
|
getAllBranches(): string[];
|
|
12
|
-
createWorktree(worktreePath: string, branch: string, baseBranch: string, copyClaudeDirectory?: boolean): {
|
|
12
|
+
createWorktree(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean): {
|
|
13
13
|
success: boolean;
|
|
14
14
|
error?: string;
|
|
15
15
|
};
|
|
@@ -27,6 +27,7 @@ export declare class WorktreeService {
|
|
|
27
27
|
success: boolean;
|
|
28
28
|
error?: string;
|
|
29
29
|
};
|
|
30
|
+
private copyClaudeSessionData;
|
|
30
31
|
hasClaudeDirectoryInBranch(branchName: string): boolean;
|
|
31
32
|
private copyClaudeDirectoryFromBaseBranch;
|
|
32
33
|
}
|
|
@@ -2,6 +2,7 @@ import { execSync } from 'child_process';
|
|
|
2
2
|
import { existsSync, statSync, cpSync } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { setWorktreeParentBranch } from '../utils/worktreeConfig.js';
|
|
5
|
+
import { getClaudeProjectsDir, pathToClaudeProjectName, } from '../utils/claudeDir.js';
|
|
5
6
|
const CLAUDE_DIR = '.claude';
|
|
6
7
|
export class WorktreeService {
|
|
7
8
|
constructor(rootPath) {
|
|
@@ -172,7 +173,7 @@ export class WorktreeService {
|
|
|
172
173
|
return [];
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
|
-
createWorktree(worktreePath, branch, baseBranch, copyClaudeDirectory = false) {
|
|
176
|
+
createWorktree(worktreePath, branch, baseBranch, copySessionData = false, copyClaudeDirectory = false) {
|
|
176
177
|
try {
|
|
177
178
|
// Resolve the worktree path relative to the git repository root
|
|
178
179
|
const resolvedPath = path.isAbsolute(worktreePath)
|
|
@@ -203,6 +204,10 @@ export class WorktreeService {
|
|
|
203
204
|
cwd: this.gitRootPath, // Execute from git root to ensure proper resolution
|
|
204
205
|
encoding: 'utf8',
|
|
205
206
|
});
|
|
207
|
+
// Copy session data if requested
|
|
208
|
+
if (copySessionData) {
|
|
209
|
+
this.copyClaudeSessionData(this.rootPath, resolvedPath);
|
|
210
|
+
}
|
|
206
211
|
// Store the parent branch in worktree config
|
|
207
212
|
try {
|
|
208
213
|
setWorktreeParentBranch(resolvedPath, baseBranch);
|
|
@@ -348,6 +353,32 @@ export class WorktreeService {
|
|
|
348
353
|
};
|
|
349
354
|
}
|
|
350
355
|
}
|
|
356
|
+
copyClaudeSessionData(sourceWorktreePath, targetWorktreePath) {
|
|
357
|
+
try {
|
|
358
|
+
const projectsDir = getClaudeProjectsDir();
|
|
359
|
+
if (!existsSync(projectsDir)) {
|
|
360
|
+
throw new Error(`Claude projects directory does not exist: ${projectsDir}`);
|
|
361
|
+
}
|
|
362
|
+
// Convert paths to Claude's naming convention
|
|
363
|
+
const sourceProjectName = pathToClaudeProjectName(sourceWorktreePath);
|
|
364
|
+
const targetProjectName = pathToClaudeProjectName(targetWorktreePath);
|
|
365
|
+
const sourceProjectDir = path.join(projectsDir, sourceProjectName);
|
|
366
|
+
const targetProjectDir = path.join(projectsDir, targetProjectName);
|
|
367
|
+
// Only copy if source project exists
|
|
368
|
+
if (existsSync(sourceProjectDir)) {
|
|
369
|
+
cpSync(sourceProjectDir, targetProjectDir, {
|
|
370
|
+
recursive: true,
|
|
371
|
+
force: true,
|
|
372
|
+
errorOnExist: false,
|
|
373
|
+
preserveTimestamps: true,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
console.error(`Failed to copy Claude session data: ${error}`);
|
|
379
|
+
throw new Error(`Failed to copy Claude session data: ${error}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
351
382
|
hasClaudeDirectoryInBranch(branchName) {
|
|
352
383
|
// Find the worktree directory for the branch
|
|
353
384
|
const worktrees = this.getWorktrees();
|
package/dist/types/index.d.ts
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utilities for Claude Code directory and project path handling.
|
|
3
|
+
* Provides functions to get Claude configuration directories respecting the
|
|
4
|
+
* CLAUDE_CONFIG_DIR environment variable and convert worktree paths to Claude's
|
|
5
|
+
* project naming convention.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Get the Claude directory path, respecting CLAUDE_CONFIG_DIR environment variable
|
|
9
|
+
* @returns The Claude directory path
|
|
10
|
+
*/
|
|
11
|
+
export declare function getClaudeDir(): string;
|
|
12
|
+
/**
|
|
13
|
+
* Get the Claude projects directory path
|
|
14
|
+
* @returns The Claude projects directory path
|
|
15
|
+
*/
|
|
16
|
+
export declare function getClaudeProjectsDir(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Convert a worktree path to Claude's project naming convention
|
|
19
|
+
* @param worktreePath The path to the worktree
|
|
20
|
+
* @returns The project name used by Claude
|
|
21
|
+
*/
|
|
22
|
+
export declare function pathToClaudeProjectName(worktreePath: string): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Utilities for Claude Code directory and project path handling.
|
|
3
|
+
* Provides functions to get Claude configuration directories respecting the
|
|
4
|
+
* CLAUDE_CONFIG_DIR environment variable and convert worktree paths to Claude's
|
|
5
|
+
* project naming convention.
|
|
6
|
+
*/
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
/**
|
|
10
|
+
* Get the Claude directory path, respecting CLAUDE_CONFIG_DIR environment variable
|
|
11
|
+
* @returns The Claude directory path
|
|
12
|
+
*/
|
|
13
|
+
export function getClaudeDir() {
|
|
14
|
+
const envConfigDir = process.env['CLAUDE_CONFIG_DIR'];
|
|
15
|
+
if (envConfigDir) {
|
|
16
|
+
return envConfigDir.trim();
|
|
17
|
+
}
|
|
18
|
+
// Default to ~/.claude for backward compatibility and when not set
|
|
19
|
+
return path.join(os.homedir(), '.claude');
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get the Claude projects directory path
|
|
23
|
+
* @returns The Claude projects directory path
|
|
24
|
+
*/
|
|
25
|
+
export function getClaudeProjectsDir() {
|
|
26
|
+
return path.join(getClaudeDir(), 'projects');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Convert a worktree path to Claude's project naming convention
|
|
30
|
+
* @param worktreePath The path to the worktree
|
|
31
|
+
* @returns The project name used by Claude
|
|
32
|
+
*/
|
|
33
|
+
export function pathToClaudeProjectName(worktreePath) {
|
|
34
|
+
// Convert absolute path to Claude's project naming convention
|
|
35
|
+
// Claude replaces all path separators and dots with dashes
|
|
36
|
+
const resolved = path.resolve(worktreePath);
|
|
37
|
+
// Handle both forward slashes (Linux/macOS) and backslashes (Windows)
|
|
38
|
+
return resolved.replace(/[/\\.]/g, '-');
|
|
39
|
+
}
|