ccmanager 1.1.1 → 1.3.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 +30 -0
- package/dist/cli.d.ts +8 -1
- package/dist/cli.js +31 -4
- package/dist/components/App.d.ts +5 -1
- package/dist/components/App.js +18 -7
- package/dist/components/Menu.d.ts +2 -0
- package/dist/components/Menu.js +13 -2
- package/dist/components/NewWorktree.d.ts +1 -1
- package/dist/components/NewWorktree.js +34 -2
- package/dist/integration-tests/devcontainer.integration.test.d.ts +1 -0
- package/dist/integration-tests/devcontainer.integration.test.js +101 -0
- package/dist/services/sessionManager.d.ts +5 -2
- package/dist/services/sessionManager.js +59 -46
- package/dist/services/sessionManager.test.js +358 -142
- package/dist/services/worktreeService.d.ts +3 -1
- package/dist/services/worktreeService.js +61 -2
- package/dist/services/worktreeService.test.js +165 -0
- package/dist/types/index.d.ts +5 -1
- package/package.json +1 -1
|
@@ -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): {
|
|
12
|
+
createWorktree(worktreePath: string, branch: string, baseBranch: string, copyClaudeDirectory?: boolean): {
|
|
13
13
|
success: boolean;
|
|
14
14
|
error?: string;
|
|
15
15
|
};
|
|
@@ -27,4 +27,6 @@ export declare class WorktreeService {
|
|
|
27
27
|
success: boolean;
|
|
28
28
|
error?: string;
|
|
29
29
|
};
|
|
30
|
+
hasClaudeDirectoryInBranch(branchName: string): boolean;
|
|
31
|
+
private copyClaudeDirectoryFromBaseBranch;
|
|
30
32
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
|
-
import { existsSync } from 'fs';
|
|
2
|
+
import { existsSync, statSync, cpSync } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { setWorktreeParentBranch } from '../utils/worktreeConfig.js';
|
|
5
|
+
const CLAUDE_DIR = '.claude';
|
|
5
6
|
export class WorktreeService {
|
|
6
7
|
constructor(rootPath) {
|
|
7
8
|
Object.defineProperty(this, "rootPath", {
|
|
@@ -171,7 +172,7 @@ export class WorktreeService {
|
|
|
171
172
|
return [];
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
|
-
createWorktree(worktreePath, branch, baseBranch) {
|
|
175
|
+
createWorktree(worktreePath, branch, baseBranch, copyClaudeDirectory = false) {
|
|
175
176
|
try {
|
|
176
177
|
// Resolve the worktree path relative to the git repository root
|
|
177
178
|
const resolvedPath = path.isAbsolute(worktreePath)
|
|
@@ -209,6 +210,15 @@ export class WorktreeService {
|
|
|
209
210
|
catch (error) {
|
|
210
211
|
console.error('Warning: Failed to set parent branch in worktree config:', error);
|
|
211
212
|
}
|
|
213
|
+
// Copy .claude directory if requested
|
|
214
|
+
if (copyClaudeDirectory) {
|
|
215
|
+
try {
|
|
216
|
+
this.copyClaudeDirectoryFromBaseBranch(resolvedPath, baseBranch);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
console.error('Warning: Failed to copy .claude directory:', error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
212
222
|
return { success: true };
|
|
213
223
|
}
|
|
214
224
|
catch (error) {
|
|
@@ -338,4 +348,53 @@ export class WorktreeService {
|
|
|
338
348
|
};
|
|
339
349
|
}
|
|
340
350
|
}
|
|
351
|
+
hasClaudeDirectoryInBranch(branchName) {
|
|
352
|
+
// Find the worktree directory for the branch
|
|
353
|
+
const worktrees = this.getWorktrees();
|
|
354
|
+
let targetWorktree = worktrees.find(wt => wt.branch && wt.branch.replace('refs/heads/', '') === branchName);
|
|
355
|
+
// If branch worktree not found, try the default branch
|
|
356
|
+
if (!targetWorktree) {
|
|
357
|
+
const defaultBranch = this.getDefaultBranch();
|
|
358
|
+
if (branchName === defaultBranch) {
|
|
359
|
+
targetWorktree = worktrees.find(wt => wt.branch && wt.branch.replace('refs/heads/', '') === defaultBranch);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// If still not found and it's the default branch, try the main worktree
|
|
363
|
+
if (!targetWorktree && branchName === this.getDefaultBranch()) {
|
|
364
|
+
targetWorktree = worktrees.find(wt => wt.isMainWorktree);
|
|
365
|
+
}
|
|
366
|
+
if (!targetWorktree) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
// Check if .claude directory exists in the worktree
|
|
370
|
+
const claudePath = path.join(targetWorktree.path, CLAUDE_DIR);
|
|
371
|
+
return existsSync(claudePath) && statSync(claudePath).isDirectory();
|
|
372
|
+
}
|
|
373
|
+
copyClaudeDirectoryFromBaseBranch(worktreePath, baseBranch) {
|
|
374
|
+
// Find the worktree directory for the base branch
|
|
375
|
+
const worktrees = this.getWorktrees();
|
|
376
|
+
let baseWorktree = worktrees.find(wt => wt.branch && wt.branch.replace('refs/heads/', '') === baseBranch);
|
|
377
|
+
// If base branch worktree not found, try the default branch
|
|
378
|
+
if (!baseWorktree) {
|
|
379
|
+
const defaultBranch = this.getDefaultBranch();
|
|
380
|
+
baseWorktree = worktrees.find(wt => wt.branch && wt.branch.replace('refs/heads/', '') === defaultBranch);
|
|
381
|
+
}
|
|
382
|
+
// If still not found, try the main worktree
|
|
383
|
+
if (!baseWorktree) {
|
|
384
|
+
baseWorktree = worktrees.find(wt => wt.isMainWorktree);
|
|
385
|
+
}
|
|
386
|
+
if (!baseWorktree) {
|
|
387
|
+
throw new Error('Could not find base worktree to copy settings from');
|
|
388
|
+
}
|
|
389
|
+
// Check if .claude directory exists in base worktree
|
|
390
|
+
const sourceClaudeDir = path.join(baseWorktree.path, CLAUDE_DIR);
|
|
391
|
+
if (!existsSync(sourceClaudeDir) ||
|
|
392
|
+
!statSync(sourceClaudeDir).isDirectory()) {
|
|
393
|
+
// No .claude directory to copy, this is fine
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
// Copy .claude directory to new worktree
|
|
397
|
+
const targetClaudeDir = path.join(worktreePath, CLAUDE_DIR);
|
|
398
|
+
cpSync(sourceClaudeDir, targetClaudeDir, { recursive: true });
|
|
399
|
+
}
|
|
341
400
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { WorktreeService } from './worktreeService.js';
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
|
+
import { existsSync, statSync } from 'fs';
|
|
4
5
|
// Mock child_process module
|
|
5
6
|
vi.mock('child_process');
|
|
7
|
+
// Mock fs module
|
|
8
|
+
vi.mock('fs');
|
|
6
9
|
// Mock worktreeConfigManager
|
|
7
10
|
vi.mock('./worktreeConfigManager.js', () => ({
|
|
8
11
|
worktreeConfigManager: {
|
|
@@ -13,6 +16,8 @@ vi.mock('./worktreeConfigManager.js', () => ({
|
|
|
13
16
|
}));
|
|
14
17
|
// Get the mocked function with proper typing
|
|
15
18
|
const mockedExecSync = vi.mocked(execSync);
|
|
19
|
+
const mockedExistsSync = vi.mocked(existsSync);
|
|
20
|
+
const mockedStatSync = vi.mocked(statSync);
|
|
16
21
|
describe('WorktreeService', () => {
|
|
17
22
|
let service;
|
|
18
23
|
beforeEach(() => {
|
|
@@ -152,4 +157,164 @@ origin/feature/test
|
|
|
152
157
|
expect(execSync).toHaveBeenCalledWith('git worktree add -b "new-feature" "/path/to/worktree" "main"', expect.any(Object));
|
|
153
158
|
});
|
|
154
159
|
});
|
|
160
|
+
describe('hasClaudeDirectoryInBranch', () => {
|
|
161
|
+
it('should return true when .claude directory exists in branch worktree', () => {
|
|
162
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
163
|
+
if (typeof cmd === 'string') {
|
|
164
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
165
|
+
return '/fake/path/.git\n';
|
|
166
|
+
}
|
|
167
|
+
if (cmd === 'git worktree list --porcelain') {
|
|
168
|
+
return `worktree /fake/path
|
|
169
|
+
HEAD abcd1234
|
|
170
|
+
branch refs/heads/main
|
|
171
|
+
|
|
172
|
+
worktree /fake/path/feature-branch
|
|
173
|
+
HEAD efgh5678
|
|
174
|
+
branch refs/heads/feature-branch
|
|
175
|
+
`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
179
|
+
});
|
|
180
|
+
mockedExistsSync.mockImplementation(path => {
|
|
181
|
+
return path === '/fake/path/feature-branch/.claude';
|
|
182
|
+
});
|
|
183
|
+
mockedStatSync.mockImplementation(() => ({
|
|
184
|
+
isDirectory: () => true,
|
|
185
|
+
}));
|
|
186
|
+
const result = service.hasClaudeDirectoryInBranch('feature-branch');
|
|
187
|
+
expect(result).toBe(true);
|
|
188
|
+
expect(existsSync).toHaveBeenCalledWith('/fake/path/feature-branch/.claude');
|
|
189
|
+
expect(statSync).toHaveBeenCalledWith('/fake/path/feature-branch/.claude');
|
|
190
|
+
});
|
|
191
|
+
it('should return false when .claude directory does not exist', () => {
|
|
192
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
193
|
+
if (typeof cmd === 'string') {
|
|
194
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
195
|
+
return '/fake/path/.git\n';
|
|
196
|
+
}
|
|
197
|
+
if (cmd === 'git worktree list --porcelain') {
|
|
198
|
+
return `worktree /fake/path
|
|
199
|
+
HEAD abcd1234
|
|
200
|
+
branch refs/heads/main
|
|
201
|
+
|
|
202
|
+
worktree /fake/path/feature-branch
|
|
203
|
+
HEAD efgh5678
|
|
204
|
+
branch refs/heads/feature-branch
|
|
205
|
+
`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
209
|
+
});
|
|
210
|
+
mockedExistsSync.mockReturnValue(false);
|
|
211
|
+
const result = service.hasClaudeDirectoryInBranch('feature-branch');
|
|
212
|
+
expect(result).toBe(false);
|
|
213
|
+
expect(existsSync).toHaveBeenCalledWith('/fake/path/feature-branch/.claude');
|
|
214
|
+
});
|
|
215
|
+
it('should return false when .claude exists but is not a directory', () => {
|
|
216
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
217
|
+
if (typeof cmd === 'string') {
|
|
218
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
219
|
+
return '/fake/path/.git\n';
|
|
220
|
+
}
|
|
221
|
+
if (cmd === 'git worktree list --porcelain') {
|
|
222
|
+
return `worktree /fake/path
|
|
223
|
+
HEAD abcd1234
|
|
224
|
+
branch refs/heads/main
|
|
225
|
+
|
|
226
|
+
worktree /fake/path/feature-branch
|
|
227
|
+
HEAD efgh5678
|
|
228
|
+
branch refs/heads/feature-branch
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
233
|
+
});
|
|
234
|
+
mockedExistsSync.mockReturnValue(true);
|
|
235
|
+
mockedStatSync.mockImplementation(() => ({
|
|
236
|
+
isDirectory: () => false,
|
|
237
|
+
}));
|
|
238
|
+
const result = service.hasClaudeDirectoryInBranch('feature-branch');
|
|
239
|
+
expect(result).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
it('should fallback to default branch when branch worktree not found', () => {
|
|
242
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
243
|
+
if (typeof cmd === 'string') {
|
|
244
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
245
|
+
return '/fake/path/.git\n';
|
|
246
|
+
}
|
|
247
|
+
if (cmd === 'git worktree list --porcelain') {
|
|
248
|
+
return `worktree /fake/path
|
|
249
|
+
HEAD abcd1234
|
|
250
|
+
branch refs/heads/main
|
|
251
|
+
`;
|
|
252
|
+
}
|
|
253
|
+
if (cmd.includes('symbolic-ref')) {
|
|
254
|
+
return 'main\n';
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
258
|
+
});
|
|
259
|
+
mockedExistsSync.mockReturnValue(true);
|
|
260
|
+
mockedStatSync.mockImplementation(() => ({
|
|
261
|
+
isDirectory: () => true,
|
|
262
|
+
}));
|
|
263
|
+
// When asking for main branch that doesn't have a separate worktree
|
|
264
|
+
const result = service.hasClaudeDirectoryInBranch('main');
|
|
265
|
+
expect(result).toBe(true);
|
|
266
|
+
expect(existsSync).toHaveBeenCalledWith('/fake/path/.claude');
|
|
267
|
+
});
|
|
268
|
+
it('should return false when branch not found in any worktree', () => {
|
|
269
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
270
|
+
if (typeof cmd === 'string') {
|
|
271
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
272
|
+
return '/fake/path/.git\n';
|
|
273
|
+
}
|
|
274
|
+
if (cmd === 'git worktree list --porcelain') {
|
|
275
|
+
return `worktree /fake/path
|
|
276
|
+
HEAD abcd1234
|
|
277
|
+
branch refs/heads/main
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
280
|
+
if (cmd.includes('symbolic-ref')) {
|
|
281
|
+
return 'main\n';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
285
|
+
});
|
|
286
|
+
const result = service.hasClaudeDirectoryInBranch('non-existent-branch');
|
|
287
|
+
expect(result).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
it('should check main worktree when branch is default branch', () => {
|
|
290
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
291
|
+
if (typeof cmd === 'string') {
|
|
292
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
293
|
+
return '/fake/path/.git\n';
|
|
294
|
+
}
|
|
295
|
+
if (cmd === 'git worktree list --porcelain') {
|
|
296
|
+
return `worktree /fake/path
|
|
297
|
+
HEAD abcd1234
|
|
298
|
+
branch refs/heads/main
|
|
299
|
+
|
|
300
|
+
worktree /fake/path/other-branch
|
|
301
|
+
HEAD efgh5678
|
|
302
|
+
branch refs/heads/other-branch
|
|
303
|
+
`;
|
|
304
|
+
}
|
|
305
|
+
if (cmd.includes('symbolic-ref')) {
|
|
306
|
+
return 'main\n';
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
310
|
+
});
|
|
311
|
+
mockedExistsSync.mockReturnValue(true);
|
|
312
|
+
mockedStatSync.mockImplementation(() => ({
|
|
313
|
+
isDirectory: () => true,
|
|
314
|
+
}));
|
|
315
|
+
const result = service.hasClaudeDirectoryInBranch('main');
|
|
316
|
+
expect(result).toBe(true);
|
|
317
|
+
expect(existsSync).toHaveBeenCalledWith('/fake/path/.claude');
|
|
318
|
+
});
|
|
319
|
+
});
|
|
155
320
|
});
|
package/dist/types/index.d.ts
CHANGED
|
@@ -26,10 +26,10 @@ export interface Session {
|
|
|
26
26
|
isPrimaryCommand?: boolean;
|
|
27
27
|
commandConfig?: CommandConfig;
|
|
28
28
|
detectionStrategy?: StateDetectionStrategy;
|
|
29
|
+
devcontainerConfig?: DevcontainerConfig;
|
|
29
30
|
}
|
|
30
31
|
export interface SessionManager {
|
|
31
32
|
sessions: Map<string, Session>;
|
|
32
|
-
createSession(worktreePath: string): Promise<Session>;
|
|
33
33
|
getSession(worktreePath: string): Session | undefined;
|
|
34
34
|
destroySession(worktreePath: string): void;
|
|
35
35
|
getAllSessions(): Session[];
|
|
@@ -76,6 +76,10 @@ export interface CommandPresetsConfig {
|
|
|
76
76
|
defaultPresetId: string;
|
|
77
77
|
selectPresetOnStart?: boolean;
|
|
78
78
|
}
|
|
79
|
+
export interface DevcontainerConfig {
|
|
80
|
+
upCommand: string;
|
|
81
|
+
execCommand: string;
|
|
82
|
+
}
|
|
79
83
|
export interface ConfigurationData {
|
|
80
84
|
shortcuts?: ShortcutConfig;
|
|
81
85
|
statusHooks?: StatusHookConfig;
|