ccmanager 3.11.2 → 3.11.3
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.
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { Worktree } from '../types/index.js';
|
|
2
2
|
/**
|
|
3
|
-
* Custom hook for polling git status of worktrees with Effect-based execution
|
|
3
|
+
* Custom hook for polling git status and commit dates of worktrees with Effect-based execution
|
|
4
4
|
*
|
|
5
|
-
* Fetches git status for each worktree at regular intervals
|
|
6
|
-
* and updates worktree state with results.
|
|
5
|
+
* Fetches git status and last commit date for each worktree at regular intervals
|
|
6
|
+
* using Effect.runPromiseExit and updates worktree state with results.
|
|
7
|
+
* Both are fetched together so they appear at the same time.
|
|
8
|
+
* Handles cancellation via AbortController.
|
|
7
9
|
*
|
|
8
10
|
* @param worktrees - Array of worktrees to monitor
|
|
9
11
|
* @param defaultBranch - Default branch for comparisons (null disables polling)
|
|
10
12
|
* @param updateInterval - Polling interval in milliseconds (default: 5000)
|
|
11
|
-
* @returns Array of worktrees with updated gitStatus and
|
|
13
|
+
* @returns Array of worktrees with updated gitStatus, gitStatusError, and lastCommitDate fields
|
|
12
14
|
*/
|
|
13
15
|
export declare function useGitStatus(worktrees: Worktree[], defaultBranch: string | null, updateInterval?: number): Worktree[];
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { Effect, Exit, Cause, Option } from 'effect';
|
|
3
|
-
import { getGitStatusLimited } from '../utils/gitStatus.js';
|
|
3
|
+
import { getGitStatusLimited, getLastCommitDateLimited, } from '../utils/gitStatus.js';
|
|
4
4
|
/**
|
|
5
|
-
* Custom hook for polling git status of worktrees with Effect-based execution
|
|
5
|
+
* Custom hook for polling git status and commit dates of worktrees with Effect-based execution
|
|
6
6
|
*
|
|
7
|
-
* Fetches git status for each worktree at regular intervals
|
|
8
|
-
* and updates worktree state with results.
|
|
7
|
+
* Fetches git status and last commit date for each worktree at regular intervals
|
|
8
|
+
* using Effect.runPromiseExit and updates worktree state with results.
|
|
9
|
+
* Both are fetched together so they appear at the same time.
|
|
10
|
+
* Handles cancellation via AbortController.
|
|
9
11
|
*
|
|
10
12
|
* @param worktrees - Array of worktrees to monitor
|
|
11
13
|
* @param defaultBranch - Default branch for comparisons (null disables polling)
|
|
12
14
|
* @param updateInterval - Polling interval in milliseconds (default: 5000)
|
|
13
|
-
* @returns Array of worktrees with updated gitStatus and
|
|
15
|
+
* @returns Array of worktrees with updated gitStatus, gitStatusError, and lastCommitDate fields
|
|
14
16
|
*/
|
|
15
17
|
export function useGitStatus(worktrees, defaultBranch, updateInterval = 5000) {
|
|
16
18
|
const [worktreesWithStatus, setWorktreesWithStatus] = useState(worktrees);
|
|
@@ -22,12 +24,17 @@ export function useGitStatus(worktrees, defaultBranch, updateInterval = 5000) {
|
|
|
22
24
|
const activeRequests = new Map();
|
|
23
25
|
let isCleanedUp = false;
|
|
24
26
|
const fetchStatus = async (worktree, abortController) => {
|
|
25
|
-
//
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
// Fetch git status and last commit date in parallel
|
|
28
|
+
const [statusExit, dateExit] = await Promise.all([
|
|
29
|
+
Effect.runPromiseExit(getGitStatusLimited(worktree.path), {
|
|
30
|
+
signal: abortController.signal,
|
|
31
|
+
}),
|
|
32
|
+
Effect.runPromiseExit(getLastCommitDateLimited(worktree.path), {
|
|
33
|
+
signal: abortController.signal,
|
|
34
|
+
}),
|
|
35
|
+
]);
|
|
36
|
+
// Update worktree state with both results at once
|
|
37
|
+
handleStatusExit(statusExit, dateExit, worktree.path, setWorktreesWithStatus);
|
|
31
38
|
};
|
|
32
39
|
const scheduleUpdate = (worktree) => {
|
|
33
40
|
const abortController = new AbortController();
|
|
@@ -62,35 +69,42 @@ export function useGitStatus(worktrees, defaultBranch, updateInterval = 5000) {
|
|
|
62
69
|
return worktreesWithStatus;
|
|
63
70
|
}
|
|
64
71
|
/**
|
|
65
|
-
* Handle the Exit
|
|
72
|
+
* Handle the Exit results from Effect.runPromiseExit and update worktree state
|
|
66
73
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
74
|
+
* Updates both gitStatus and lastCommitDate in a single state update so they
|
|
75
|
+
* appear at the same time in the UI.
|
|
69
76
|
*
|
|
70
|
-
* @param
|
|
77
|
+
* @param statusExit - Exit result from git status Effect
|
|
78
|
+
* @param dateExit - Exit result from commit date Effect
|
|
71
79
|
* @param worktreePath - Path of the worktree being updated
|
|
72
80
|
* @param setWorktreesWithStatus - State setter function
|
|
73
81
|
*/
|
|
74
|
-
function handleStatusExit(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
function handleStatusExit(statusExit, dateExit, worktreePath, setWorktreesWithStatus) {
|
|
83
|
+
// Build the update object from both results
|
|
84
|
+
const update = {};
|
|
85
|
+
let hasUpdate = false;
|
|
86
|
+
if (Exit.isSuccess(statusExit)) {
|
|
87
|
+
update.gitStatus = statusExit.value;
|
|
88
|
+
update.gitStatusError = undefined;
|
|
89
|
+
hasUpdate = true;
|
|
81
90
|
}
|
|
82
|
-
else if (Exit.isFailure(
|
|
83
|
-
|
|
84
|
-
const failure = Cause.failureOption(exit.cause);
|
|
91
|
+
else if (Exit.isFailure(statusExit)) {
|
|
92
|
+
const failure = Cause.failureOption(statusExit.cause);
|
|
85
93
|
if (Option.isSome(failure)) {
|
|
86
94
|
const gitError = failure.value;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
: wt));
|
|
95
|
+
update.gitStatus = undefined;
|
|
96
|
+
update.gitStatusError = formatGitError(gitError);
|
|
97
|
+
hasUpdate = true;
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
|
-
|
|
100
|
+
if (Exit.isSuccess(dateExit)) {
|
|
101
|
+
update.lastCommitDate = dateExit.value;
|
|
102
|
+
hasUpdate = true;
|
|
103
|
+
}
|
|
104
|
+
// Silently ignore commit date errors (e.g., empty repo)
|
|
105
|
+
if (hasUpdate) {
|
|
106
|
+
setWorktreesWithStatus(prev => prev.map(wt => (wt.path === worktreePath ? { ...wt, ...update } : wt)));
|
|
107
|
+
}
|
|
94
108
|
}
|
|
95
109
|
/**
|
|
96
110
|
* Format GitError into a user-friendly error message
|
|
@@ -4,14 +4,16 @@ import { render, cleanup } from 'ink-testing-library';
|
|
|
4
4
|
import { Text } from 'ink';
|
|
5
5
|
import { Effect, Exit } from 'effect';
|
|
6
6
|
import { useGitStatus } from './useGitStatus.js';
|
|
7
|
-
import { getGitStatusLimited } from '../utils/gitStatus.js';
|
|
7
|
+
import { getGitStatusLimited, getLastCommitDateLimited, } from '../utils/gitStatus.js';
|
|
8
8
|
import { GitError } from '../types/errors.js';
|
|
9
9
|
// Mock the gitStatus module
|
|
10
10
|
vi.mock('../utils/gitStatus.js', () => ({
|
|
11
11
|
getGitStatusLimited: vi.fn(),
|
|
12
|
+
getLastCommitDateLimited: vi.fn(),
|
|
12
13
|
}));
|
|
13
14
|
describe('useGitStatus', () => {
|
|
14
15
|
const mockGetGitStatus = getGitStatusLimited;
|
|
16
|
+
const mockGetLastCommitDate = getLastCommitDateLimited;
|
|
15
17
|
const createWorktree = (path) => ({
|
|
16
18
|
path,
|
|
17
19
|
branch: 'main',
|
|
@@ -28,6 +30,9 @@ describe('useGitStatus', () => {
|
|
|
28
30
|
beforeEach(() => {
|
|
29
31
|
vi.useFakeTimers();
|
|
30
32
|
mockGetGitStatus.mockClear();
|
|
33
|
+
mockGetLastCommitDate.mockClear();
|
|
34
|
+
// Default: return a date for all worktrees
|
|
35
|
+
mockGetLastCommitDate.mockReturnValue(Effect.succeed(new Date('2025-01-01T00:00:00Z')));
|
|
31
36
|
});
|
|
32
37
|
afterEach(() => {
|
|
33
38
|
vi.useRealTimers();
|
|
@@ -123,6 +128,13 @@ describe('useGitStatus', () => {
|
|
|
123
128
|
resolveEffect = resume;
|
|
124
129
|
});
|
|
125
130
|
});
|
|
131
|
+
// Also make commit date async so Promise.all waits for both
|
|
132
|
+
let resolveDateEffect = null;
|
|
133
|
+
mockGetLastCommitDate.mockImplementation(() => {
|
|
134
|
+
return Effect.async(resume => {
|
|
135
|
+
resolveDateEffect = resume;
|
|
136
|
+
});
|
|
137
|
+
});
|
|
126
138
|
const TestComponent = () => {
|
|
127
139
|
useGitStatus(worktrees, 'main', 100);
|
|
128
140
|
return React.createElement(Text, null, 'test');
|
|
@@ -136,8 +148,9 @@ describe('useGitStatus', () => {
|
|
|
136
148
|
await vi.advanceTimersByTimeAsync(250);
|
|
137
149
|
// Should not have started a second fetch yet
|
|
138
150
|
expect(mockGetGitStatus).toHaveBeenCalledTimes(1);
|
|
139
|
-
// Complete the first fetch
|
|
151
|
+
// Complete the first fetch (both status and date)
|
|
140
152
|
resolveEffect(Exit.succeed(createGitStatus(1, 0)));
|
|
153
|
+
resolveDateEffect(Exit.succeed(new Date('2025-01-01T00:00:00Z')));
|
|
141
154
|
// Wait for the promise to resolve
|
|
142
155
|
await vi.waitFor(() => {
|
|
143
156
|
expect(fetchCount).toBe(1);
|
|
@@ -162,6 +175,12 @@ describe('useGitStatus', () => {
|
|
|
162
175
|
});
|
|
163
176
|
});
|
|
164
177
|
});
|
|
178
|
+
// Also make commit date async so it doesn't resolve before status
|
|
179
|
+
mockGetLastCommitDate.mockImplementation(() => {
|
|
180
|
+
return Effect.async(_resume => {
|
|
181
|
+
return Effect.sync(() => { });
|
|
182
|
+
});
|
|
183
|
+
});
|
|
165
184
|
const TestComponent = ({ worktrees }) => {
|
|
166
185
|
useGitStatus(worktrees, 'main', 100);
|
|
167
186
|
return React.createElement(Text, null, 'test');
|
|
@@ -662,21 +662,6 @@ export class WorktreeService {
|
|
|
662
662
|
if (mainWorktree && mainWorktree.path.includes('.git/modules')) {
|
|
663
663
|
mainWorktree.path = self.gitRootPath;
|
|
664
664
|
}
|
|
665
|
-
// Fetch last commit date for each worktree
|
|
666
|
-
for (const wt of worktrees) {
|
|
667
|
-
try {
|
|
668
|
-
const dateStr = execSync('git log -1 --format=%aI', {
|
|
669
|
-
cwd: wt.path,
|
|
670
|
-
encoding: 'utf8',
|
|
671
|
-
}).trim();
|
|
672
|
-
if (dateStr) {
|
|
673
|
-
wt.lastCommitDate = new Date(dateStr);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
catch {
|
|
677
|
-
// Ignore errors (e.g., empty repo)
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
665
|
// Sort worktrees by last session if requested
|
|
681
666
|
if (sortByLastSession) {
|
|
682
667
|
worktrees.sort((a, b) => {
|
|
@@ -41,6 +41,14 @@ export interface GitStatus {
|
|
|
41
41
|
*/
|
|
42
42
|
export declare const getGitStatus: (worktreePath: string) => Effect.Effect<GitStatus, GitError>;
|
|
43
43
|
export declare const getGitStatusLimited: (worktreePath: string) => Effect.Effect<GitStatus, GitError, never>;
|
|
44
|
+
/**
|
|
45
|
+
* Get the last commit date for a worktree
|
|
46
|
+
*
|
|
47
|
+
* @param worktreePath - Absolute path to the worktree directory
|
|
48
|
+
* @returns Effect containing the commit date or GitError
|
|
49
|
+
*/
|
|
50
|
+
export declare const getLastCommitDate: (worktreePath: string) => Effect.Effect<Date, GitError>;
|
|
51
|
+
export declare const getLastCommitDateLimited: (worktreePath: string) => Effect.Effect<Date, GitError, never>;
|
|
44
52
|
export declare function formatGitFileChanges(status: GitStatus): string;
|
|
45
53
|
export declare function formatGitAheadBehind(status: GitStatus): string;
|
|
46
54
|
export declare function formatGitStatus(status: GitStatus): string;
|
package/dist/utils/gitStatus.js
CHANGED
|
@@ -62,6 +62,24 @@ export const getGitStatus = (worktreePath) => Effect.gen(function* () {
|
|
|
62
62
|
};
|
|
63
63
|
});
|
|
64
64
|
export const getGitStatusLimited = createEffectConcurrencyLimited((worktreePath) => getGitStatus(worktreePath), 10);
|
|
65
|
+
/**
|
|
66
|
+
* Get the last commit date for a worktree
|
|
67
|
+
*
|
|
68
|
+
* @param worktreePath - Absolute path to the worktree directory
|
|
69
|
+
* @returns Effect containing the commit date or GitError
|
|
70
|
+
*/
|
|
71
|
+
export const getLastCommitDate = (worktreePath) => Effect.flatMap(runGit(['log', '-1', '--format=%aI'], worktreePath), result => {
|
|
72
|
+
const dateStr = result.stdout.trim();
|
|
73
|
+
if (dateStr) {
|
|
74
|
+
return Effect.succeed(new Date(dateStr));
|
|
75
|
+
}
|
|
76
|
+
return Effect.fail(new GitError({
|
|
77
|
+
command: 'git log -1 --format=%aI',
|
|
78
|
+
exitCode: 0,
|
|
79
|
+
stderr: 'No commits found',
|
|
80
|
+
}));
|
|
81
|
+
});
|
|
82
|
+
export const getLastCommitDateLimited = createEffectConcurrencyLimited((worktreePath) => getLastCommitDate(worktreePath), 10);
|
|
65
83
|
export function formatGitFileChanges(status) {
|
|
66
84
|
const parts = [];
|
|
67
85
|
const colors = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.3",
|
|
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.11.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "3.11.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "3.11.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "3.11.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "3.11.
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "3.11.3",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "3.11.3",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "3.11.3",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "3.11.3",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "3.11.3"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|