ccmanager 3.6.3 → 3.6.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.
@@ -28,7 +28,7 @@ export class ClaudeStateDetector extends BaseStateDetector {
28
28
  const lines = this.getTerminalLines(terminal, 3);
29
29
  const content = lines.join('\n').toLowerCase();
30
30
  // Check for "N background task(s)" pattern first (content already lowercased)
31
- const countMatch = content.match(/(\d+)\s+background\s+task/);
31
+ const countMatch = content.match(/(\d+)\s+(?:background\s+task|local\s+agent)/);
32
32
  if (countMatch?.[1]) {
33
33
  return parseInt(countMatch[1], 10);
34
34
  }
@@ -387,6 +387,49 @@ describe('ClaudeStateDetector', () => {
387
387
  // Assert
388
388
  expect(count).toBe(1);
389
389
  });
390
+ it('should return count 3 when "3 local agents" is in status bar', () => {
391
+ // Arrange
392
+ terminal = createMockTerminal([
393
+ 'Some output',
394
+ 'More output',
395
+ 'bypass permissions on - 3 local agents',
396
+ ]);
397
+ // Act
398
+ const count = detector.detectBackgroundTask(terminal);
399
+ // Assert
400
+ expect(count).toBe(3);
401
+ });
402
+ it('should return count 1 when "1 local agent" is in status bar', () => {
403
+ // Arrange
404
+ terminal = createMockTerminal(['Some output', '1 local agent']);
405
+ // Act
406
+ const count = detector.detectBackgroundTask(terminal);
407
+ // Assert
408
+ expect(count).toBe(1);
409
+ });
410
+ it('should detect local agent count case-insensitively', () => {
411
+ // Arrange
412
+ terminal = createMockTerminal([
413
+ 'Output line 1',
414
+ 'Output line 2',
415
+ '2 LOCAL AGENTS running',
416
+ ]);
417
+ // Act
418
+ const count = detector.detectBackgroundTask(terminal);
419
+ // Assert
420
+ expect(count).toBe(2);
421
+ });
422
+ it('should prioritize explicit count from "N local agents" over "(running)"', () => {
423
+ // Arrange
424
+ terminal = createMockTerminal([
425
+ 'Some output',
426
+ '3 local agents | task1 (running)',
427
+ ]);
428
+ // Act
429
+ const count = detector.detectBackgroundTask(terminal);
430
+ // Assert
431
+ expect(count).toBe(3);
432
+ });
390
433
  it('should prioritize count from "N background task" over "(running)"', () => {
391
434
  // Arrange - both patterns present, count should be from explicit pattern
392
435
  terminal = createMockTerminal([
@@ -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
+ });
@@ -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
@@ -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",
3
+ "version": "3.6.5",
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.3",
45
- "@kodaikabasawa/ccmanager-darwin-x64": "3.6.3",
46
- "@kodaikabasawa/ccmanager-linux-arm64": "3.6.3",
47
- "@kodaikabasawa/ccmanager-linux-x64": "3.6.3",
48
- "@kodaikabasawa/ccmanager-win32-x64": "3.6.3"
44
+ "@kodaikabasawa/ccmanager-darwin-arm64": "3.6.5",
45
+ "@kodaikabasawa/ccmanager-darwin-x64": "3.6.5",
46
+ "@kodaikabasawa/ccmanager-linux-arm64": "3.6.5",
47
+ "@kodaikabasawa/ccmanager-linux-x64": "3.6.5",
48
+ "@kodaikabasawa/ccmanager-win32-x64": "3.6.5"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@eslint/js": "^9.28.0",