ccmanager 3.6.3 → 3.6.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/dist/services/worktreeService.js +15 -0
- package/dist/services/worktreeService.submodule.test.d.ts +1 -0
- package/dist/services/worktreeService.submodule.test.js +82 -0
- package/dist/utils/gitUtils.d.ts +1 -0
- package/dist/utils/gitUtils.js +11 -0
- package/dist/utils/gitUtils.submodule.test.d.ts +1 -0
- package/dist/utils/gitUtils.submodule.test.js +66 -0
- package/dist/utils/worktreeUtils.js +9 -0
- package/dist/utils/worktreeUtils.submodule.test.d.ts +1 -0
- package/dist/utils/worktreeUtils.submodule.test.js +63 -0
- package/package.json +6 -6
|
@@ -72,6 +72,15 @@ export class WorktreeService {
|
|
|
72
72
|
const absoluteGitCommonDir = path.isAbsolute(gitCommonDir)
|
|
73
73
|
? gitCommonDir
|
|
74
74
|
: path.resolve(this.rootPath, gitCommonDir);
|
|
75
|
+
// Handle submodule paths: if path contains .git/modules, use --show-toplevel
|
|
76
|
+
// to get the submodule's actual working directory
|
|
77
|
+
if (absoluteGitCommonDir.includes('.git/modules')) {
|
|
78
|
+
const toplevel = execSync('git rev-parse --show-toplevel', {
|
|
79
|
+
cwd: this.rootPath,
|
|
80
|
+
encoding: 'utf8',
|
|
81
|
+
}).trim();
|
|
82
|
+
return toplevel;
|
|
83
|
+
}
|
|
75
84
|
// Handle worktree paths: if path contains .git/worktrees, we need to find the real .git parent
|
|
76
85
|
if (absoluteGitCommonDir.includes('.git/worktrees')) {
|
|
77
86
|
// Extract the path up to and including .git
|
|
@@ -647,6 +656,12 @@ export class WorktreeService {
|
|
|
647
656
|
if (worktrees.length > 0 && !worktrees.some(w => w.isMainWorktree)) {
|
|
648
657
|
worktrees[0].isMainWorktree = true;
|
|
649
658
|
}
|
|
659
|
+
// Handle submodule paths: if the main worktree path contains .git/modules,
|
|
660
|
+
// replace it with the actual working directory (self.gitRootPath)
|
|
661
|
+
const mainWorktree = worktrees.find(w => w.isMainWorktree);
|
|
662
|
+
if (mainWorktree && mainWorktree.path.includes('.git/modules')) {
|
|
663
|
+
mainWorktree.path = self.gitRootPath;
|
|
664
|
+
}
|
|
650
665
|
// Sort worktrees by last session if requested
|
|
651
666
|
if (sortByLastSession) {
|
|
652
667
|
worktrees.sort((a, b) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { Effect } from 'effect';
|
|
7
|
+
import { WorktreeService } from './worktreeService.js';
|
|
8
|
+
describe('WorktreeService with submodules', () => {
|
|
9
|
+
// Use os.tmpdir() and unique suffix to avoid conflicts with parallel tests
|
|
10
|
+
// Use realpathSync to resolve symlinks (e.g., /var -> /private/var on macOS)
|
|
11
|
+
const testDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ccmanager-ws-submodule-test-')));
|
|
12
|
+
const rootProjectDir = path.join(testDir, 'root-project');
|
|
13
|
+
const submodule1Dir = path.join(rootProjectDir, 'modules', 'submodule-1');
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
// Allow file:// protocol for local submodule cloning (needed for CI)
|
|
16
|
+
execSync('git config --global protocol.file.allow always');
|
|
17
|
+
// Clean up if exists
|
|
18
|
+
if (fs.existsSync(testDir)) {
|
|
19
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
// Create test directory structure
|
|
22
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
23
|
+
// Create submodule source repository
|
|
24
|
+
const submodule1Source = path.join(testDir, 'submodule-1-source');
|
|
25
|
+
fs.mkdirSync(submodule1Source, { recursive: true });
|
|
26
|
+
execSync('git init', { cwd: submodule1Source });
|
|
27
|
+
// Set git user for CI environment
|
|
28
|
+
execSync('git config user.email "test@test.com"', { cwd: submodule1Source });
|
|
29
|
+
execSync('git config user.name "Test User"', { cwd: submodule1Source });
|
|
30
|
+
fs.writeFileSync(path.join(submodule1Source, 'README.md'), '# Submodule 1');
|
|
31
|
+
execSync('git add README.md', { cwd: submodule1Source });
|
|
32
|
+
execSync('git commit -m "Initial commit"', { cwd: submodule1Source });
|
|
33
|
+
// Create root project
|
|
34
|
+
fs.mkdirSync(rootProjectDir, { recursive: true });
|
|
35
|
+
execSync('git init', { cwd: rootProjectDir });
|
|
36
|
+
// Set git user for CI environment
|
|
37
|
+
execSync('git config user.email "test@test.com"', { cwd: rootProjectDir });
|
|
38
|
+
execSync('git config user.name "Test User"', { cwd: rootProjectDir });
|
|
39
|
+
fs.writeFileSync(path.join(rootProjectDir, 'README.md'), '# Root Project');
|
|
40
|
+
execSync('git add README.md', { cwd: rootProjectDir });
|
|
41
|
+
execSync('git commit -m "Initial commit"', { cwd: rootProjectDir });
|
|
42
|
+
// Add submodule
|
|
43
|
+
execSync(`git submodule add ${submodule1Source} modules/submodule-1`, {
|
|
44
|
+
cwd: rootProjectDir,
|
|
45
|
+
});
|
|
46
|
+
execSync('git commit -m "Add submodule"', { cwd: rootProjectDir });
|
|
47
|
+
});
|
|
48
|
+
afterAll(() => {
|
|
49
|
+
// Clean up
|
|
50
|
+
if (fs.existsSync(testDir)) {
|
|
51
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
it('should return the submodule working directory from getGitRootPath()', () => {
|
|
55
|
+
const service = new WorktreeService(submodule1Dir);
|
|
56
|
+
const result = service.getGitRootPath();
|
|
57
|
+
// Should return the submodule's working directory
|
|
58
|
+
expect(result).toBe(submodule1Dir);
|
|
59
|
+
// Should NOT return a path containing .git/modules
|
|
60
|
+
expect(result).not.toContain('.git/modules');
|
|
61
|
+
});
|
|
62
|
+
it('should still work for regular repositories', () => {
|
|
63
|
+
const service = new WorktreeService(rootProjectDir);
|
|
64
|
+
const result = service.getGitRootPath();
|
|
65
|
+
expect(result).toBe(rootProjectDir);
|
|
66
|
+
expect(path.basename(result)).toBe('root-project');
|
|
67
|
+
});
|
|
68
|
+
it('should return worktrees with correct paths (not .git/modules paths) for submodules', async () => {
|
|
69
|
+
const service = new WorktreeService(submodule1Dir);
|
|
70
|
+
const worktrees = await Effect.runPromise(service.getWorktreesEffect());
|
|
71
|
+
// Should have at least one worktree
|
|
72
|
+
expect(worktrees.length).toBeGreaterThan(0);
|
|
73
|
+
// The main worktree path should be the submodule working directory
|
|
74
|
+
const mainWorktree = worktrees.find(wt => wt.isMainWorktree);
|
|
75
|
+
expect(mainWorktree).toBeDefined();
|
|
76
|
+
expect(mainWorktree.path).toBe(submodule1Dir);
|
|
77
|
+
// No worktree should have .git/modules in its path
|
|
78
|
+
for (const wt of worktrees) {
|
|
79
|
+
expect(wt.path).not.toContain('.git/modules');
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
package/dist/utils/gitUtils.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Get the git repository root path from a given directory.
|
|
3
3
|
* For worktrees, this returns the main repository root (parent of .git).
|
|
4
|
+
* For submodules, this returns the submodule's working directory.
|
|
4
5
|
*
|
|
5
6
|
* @param cwd - The directory to start searching from
|
|
6
7
|
* @returns The absolute path to the git repository root, or null if not in a git repo
|
package/dist/utils/gitUtils.js
CHANGED
|
@@ -3,6 +3,7 @@ import { execSync } from 'child_process';
|
|
|
3
3
|
/**
|
|
4
4
|
* Get the git repository root path from a given directory.
|
|
5
5
|
* For worktrees, this returns the main repository root (parent of .git).
|
|
6
|
+
* For submodules, this returns the submodule's working directory.
|
|
6
7
|
*
|
|
7
8
|
* @param cwd - The directory to start searching from
|
|
8
9
|
* @returns The absolute path to the git repository root, or null if not in a git repo
|
|
@@ -17,6 +18,16 @@ export function getGitRepositoryRoot(cwd) {
|
|
|
17
18
|
const absoluteGitCommonDir = path.isAbsolute(gitCommonDir)
|
|
18
19
|
? gitCommonDir
|
|
19
20
|
: path.resolve(cwd, gitCommonDir);
|
|
21
|
+
// Handle submodule paths: if path contains .git/modules, use --show-toplevel
|
|
22
|
+
// to get the submodule's actual working directory
|
|
23
|
+
if (absoluteGitCommonDir.includes('.git/modules')) {
|
|
24
|
+
const toplevel = execSync('git rev-parse --show-toplevel', {
|
|
25
|
+
cwd,
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
28
|
+
}).trim();
|
|
29
|
+
return toplevel;
|
|
30
|
+
}
|
|
20
31
|
// Handle worktree paths: if path contains .git/worktrees, find the real .git parent
|
|
21
32
|
if (absoluteGitCommonDir.includes('.git/worktrees')) {
|
|
22
33
|
const gitIndex = absoluteGitCommonDir.indexOf('.git');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { getGitRepositoryRoot } from './gitUtils.js';
|
|
7
|
+
describe('getGitRepositoryRoot with submodules', () => {
|
|
8
|
+
// Use os.tmpdir() and unique suffix to avoid conflicts with parallel tests
|
|
9
|
+
// Use realpathSync to resolve symlinks (e.g., /var -> /private/var on macOS)
|
|
10
|
+
const testDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ccmanager-submodule-test-')));
|
|
11
|
+
const rootProjectDir = path.join(testDir, 'root-project');
|
|
12
|
+
const submodule1Dir = path.join(rootProjectDir, 'modules', 'submodule-1');
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
// Allow file:// protocol for local submodule cloning (needed for CI)
|
|
15
|
+
execSync('git config --global protocol.file.allow always');
|
|
16
|
+
// Clean up if exists
|
|
17
|
+
if (fs.existsSync(testDir)) {
|
|
18
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
// Create test directory structure
|
|
21
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
22
|
+
// Create submodule source repository
|
|
23
|
+
const submodule1Source = path.join(testDir, 'submodule-1-source');
|
|
24
|
+
fs.mkdirSync(submodule1Source, { recursive: true });
|
|
25
|
+
execSync('git init', { cwd: submodule1Source });
|
|
26
|
+
// Set git user for CI environment
|
|
27
|
+
execSync('git config user.email "test@test.com"', { cwd: submodule1Source });
|
|
28
|
+
execSync('git config user.name "Test User"', { cwd: submodule1Source });
|
|
29
|
+
fs.writeFileSync(path.join(submodule1Source, 'README.md'), '# Submodule 1');
|
|
30
|
+
execSync('git add README.md', { cwd: submodule1Source });
|
|
31
|
+
execSync('git commit -m "Initial commit"', { cwd: submodule1Source });
|
|
32
|
+
// Create root project
|
|
33
|
+
fs.mkdirSync(rootProjectDir, { recursive: true });
|
|
34
|
+
execSync('git init', { cwd: rootProjectDir });
|
|
35
|
+
// Set git user for CI environment
|
|
36
|
+
execSync('git config user.email "test@test.com"', { cwd: rootProjectDir });
|
|
37
|
+
execSync('git config user.name "Test User"', { cwd: rootProjectDir });
|
|
38
|
+
fs.writeFileSync(path.join(rootProjectDir, 'README.md'), '# Root Project');
|
|
39
|
+
execSync('git add README.md', { cwd: rootProjectDir });
|
|
40
|
+
execSync('git commit -m "Initial commit"', { cwd: rootProjectDir });
|
|
41
|
+
// Add submodule
|
|
42
|
+
execSync(`git submodule add ${submodule1Source} modules/submodule-1`, {
|
|
43
|
+
cwd: rootProjectDir,
|
|
44
|
+
});
|
|
45
|
+
execSync('git commit -m "Add submodule"', { cwd: rootProjectDir });
|
|
46
|
+
});
|
|
47
|
+
afterAll(() => {
|
|
48
|
+
// Clean up
|
|
49
|
+
if (fs.existsSync(testDir)) {
|
|
50
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
it('should return the submodule working directory, not the parent .git/modules path', () => {
|
|
54
|
+
// When running from within a submodule
|
|
55
|
+
const result = getGitRepositoryRoot(submodule1Dir);
|
|
56
|
+
// Should return the submodule's working directory
|
|
57
|
+
expect(result).toBe(submodule1Dir);
|
|
58
|
+
// Should NOT return a path containing .git/modules
|
|
59
|
+
expect(result).not.toContain('.git/modules');
|
|
60
|
+
});
|
|
61
|
+
it('should still work for regular repositories', () => {
|
|
62
|
+
const result = getGitRepositoryRoot(rootProjectDir);
|
|
63
|
+
expect(result).toBe(rootProjectDir);
|
|
64
|
+
expect(path.basename(result)).toBe('root-project');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -21,6 +21,15 @@ function getGitRepositoryName(projectPath) {
|
|
|
21
21
|
const absoluteGitCommonDir = path.isAbsolute(gitCommonDir)
|
|
22
22
|
? gitCommonDir
|
|
23
23
|
: path.resolve(projectPath, gitCommonDir);
|
|
24
|
+
// Handle submodule paths: if path contains .git/modules, use --show-toplevel
|
|
25
|
+
// to get the submodule's actual working directory
|
|
26
|
+
if (absoluteGitCommonDir.includes('.git/modules')) {
|
|
27
|
+
const toplevel = execSync('git rev-parse --show-toplevel', {
|
|
28
|
+
cwd: projectPath,
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
}).trim();
|
|
31
|
+
return path.basename(toplevel);
|
|
32
|
+
}
|
|
24
33
|
const mainWorkingDir = path.dirname(absoluteGitCommonDir);
|
|
25
34
|
return path.basename(mainWorkingDir);
|
|
26
35
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { generateWorktreeDirectory } from './worktreeUtils.js';
|
|
7
|
+
describe('generateWorktreeDirectory with submodules', () => {
|
|
8
|
+
// Use os.tmpdir() and unique suffix to avoid conflicts with parallel tests
|
|
9
|
+
// Use realpathSync to resolve symlinks (e.g., /var -> /private/var on macOS)
|
|
10
|
+
const testDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ccmanager-wt-submodule-test-')));
|
|
11
|
+
const rootProjectDir = path.join(testDir, 'root-project');
|
|
12
|
+
const submodule1Dir = path.join(rootProjectDir, 'modules', 'submodule-1');
|
|
13
|
+
beforeAll(() => {
|
|
14
|
+
// Allow file:// protocol for local submodule cloning (needed for CI)
|
|
15
|
+
execSync('git config --global protocol.file.allow always');
|
|
16
|
+
// Clean up if exists
|
|
17
|
+
if (fs.existsSync(testDir)) {
|
|
18
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
19
|
+
}
|
|
20
|
+
// Create test directory structure
|
|
21
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
22
|
+
// Create submodule source repository
|
|
23
|
+
const submodule1Source = path.join(testDir, 'submodule-1-source');
|
|
24
|
+
fs.mkdirSync(submodule1Source, { recursive: true });
|
|
25
|
+
execSync('git init', { cwd: submodule1Source });
|
|
26
|
+
// Set git user for CI environment
|
|
27
|
+
execSync('git config user.email "test@test.com"', { cwd: submodule1Source });
|
|
28
|
+
execSync('git config user.name "Test User"', { cwd: submodule1Source });
|
|
29
|
+
fs.writeFileSync(path.join(submodule1Source, 'README.md'), '# Submodule 1');
|
|
30
|
+
execSync('git add README.md', { cwd: submodule1Source });
|
|
31
|
+
execSync('git commit -m "Initial commit"', { cwd: submodule1Source });
|
|
32
|
+
// Create root project
|
|
33
|
+
fs.mkdirSync(rootProjectDir, { recursive: true });
|
|
34
|
+
execSync('git init', { cwd: rootProjectDir });
|
|
35
|
+
// Set git user for CI environment
|
|
36
|
+
execSync('git config user.email "test@test.com"', { cwd: rootProjectDir });
|
|
37
|
+
execSync('git config user.name "Test User"', { cwd: rootProjectDir });
|
|
38
|
+
fs.writeFileSync(path.join(rootProjectDir, 'README.md'), '# Root Project');
|
|
39
|
+
execSync('git add README.md', { cwd: rootProjectDir });
|
|
40
|
+
execSync('git commit -m "Initial commit"', { cwd: rootProjectDir });
|
|
41
|
+
// Add submodule
|
|
42
|
+
execSync(`git submodule add ${submodule1Source} modules/submodule-1`, {
|
|
43
|
+
cwd: rootProjectDir,
|
|
44
|
+
});
|
|
45
|
+
execSync('git commit -m "Add submodule"', { cwd: rootProjectDir });
|
|
46
|
+
});
|
|
47
|
+
afterAll(() => {
|
|
48
|
+
// Clean up
|
|
49
|
+
if (fs.existsSync(testDir)) {
|
|
50
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
it('should use submodule name (not "modules") in {project} placeholder', () => {
|
|
54
|
+
const result = generateWorktreeDirectory(submodule1Dir, 'feature/test', '../worktrees/{project}-{branch}');
|
|
55
|
+
// Should contain "submodule-1", not "modules"
|
|
56
|
+
expect(result).toContain('submodule-1');
|
|
57
|
+
expect(result).not.toContain('modules-');
|
|
58
|
+
});
|
|
59
|
+
it('should work correctly for regular repositories', () => {
|
|
60
|
+
const result = generateWorktreeDirectory(rootProjectDir, 'feature/test', '../worktrees/{project}-{branch}');
|
|
61
|
+
expect(result).toContain('root-project');
|
|
62
|
+
});
|
|
63
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.4",
|
|
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": "3.6.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "3.6.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "3.6.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "3.6.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "3.6.
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "3.6.4",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "3.6.4",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "3.6.4",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "3.6.4",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "3.6.4"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|