ccmanager 4.1.19 → 4.1.20
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/components/App.js
CHANGED
|
@@ -18,7 +18,6 @@ import { globalSessionOrchestrator } from '../services/globalSessionOrchestrator
|
|
|
18
18
|
import { WorktreeService } from '../services/worktreeService.js';
|
|
19
19
|
import { worktreeNameGenerator, generateFallbackBranchName, } from '../services/worktreeNameGenerator.js';
|
|
20
20
|
import { logger } from '../utils/logger.js';
|
|
21
|
-
import { AmbiguousBranchError, } from '../types/index.js';
|
|
22
21
|
import { configReader } from '../services/config/configReader.js';
|
|
23
22
|
import { ENV_VARS } from '../constants/env.js';
|
|
24
23
|
import { MULTI_PROJECT_ERRORS } from '../constants/error.js';
|
|
@@ -217,29 +216,6 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
217
216
|
cancelled = true;
|
|
218
217
|
};
|
|
219
218
|
}, [view, pendingMenuSessionLaunch, startSessionForWorktree]);
|
|
220
|
-
// Helper function to parse ambiguous branch error and create AmbiguousBranchError
|
|
221
|
-
const parseAmbiguousBranchError = (errorMessage) => {
|
|
222
|
-
const pattern = /Ambiguous branch '(.+?)' found in multiple remotes: (.+?)\. Please specify which remote to use\./;
|
|
223
|
-
const match = errorMessage.match(pattern);
|
|
224
|
-
if (!match) {
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
const branchName = match[1];
|
|
228
|
-
const remoteRefsText = match[2];
|
|
229
|
-
const remoteRefs = remoteRefsText.split(', ');
|
|
230
|
-
// Parse remote refs into RemoteBranchMatch objects
|
|
231
|
-
const matches = remoteRefs.map(fullRef => {
|
|
232
|
-
const parts = fullRef.split('/');
|
|
233
|
-
const remote = parts[0];
|
|
234
|
-
const branch = parts.slice(1).join('/');
|
|
235
|
-
return {
|
|
236
|
-
remote,
|
|
237
|
-
branch,
|
|
238
|
-
fullRef,
|
|
239
|
-
};
|
|
240
|
-
});
|
|
241
|
-
return new AmbiguousBranchError(branchName, matches);
|
|
242
|
-
};
|
|
243
219
|
// Helper function to handle worktree creation results
|
|
244
220
|
const handleWorktreeCreationResult = (result, creationData) => {
|
|
245
221
|
if (result.success) {
|
|
@@ -266,21 +242,8 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
266
242
|
handleReturnToMenu();
|
|
267
243
|
return;
|
|
268
244
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (ambiguousError) {
|
|
272
|
-
// Handle ambiguous branch error
|
|
273
|
-
setPendingWorktreeCreation({
|
|
274
|
-
...creationData,
|
|
275
|
-
ambiguousError,
|
|
276
|
-
});
|
|
277
|
-
navigateWithClear('remote-branch-selector');
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
// Handle regular error
|
|
281
|
-
setError(errorMessage);
|
|
282
|
-
setView('new-worktree');
|
|
283
|
-
}
|
|
245
|
+
setError(result.error || 'Failed to create worktree');
|
|
246
|
+
setView('new-worktree');
|
|
284
247
|
};
|
|
285
248
|
const handleMenuAction = async (action) => {
|
|
286
249
|
switch (action.type) {
|
|
@@ -409,9 +372,23 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
409
372
|
setView('creating-worktree');
|
|
410
373
|
// Create the worktree using Effect
|
|
411
374
|
const result = await Effect.runPromise(Effect.either(worktreeService.createWorktreeEffect(targetPath, branch, request.baseBranch, request.copySessionData, request.copyClaudeDirectory)));
|
|
412
|
-
// Transform Effect result to legacy format for handleWorktreeCreationResult
|
|
413
375
|
if (result._tag === 'Left') {
|
|
414
|
-
|
|
376
|
+
if (result.left._tag === 'AmbiguousBranchError') {
|
|
377
|
+
setPendingWorktreeCreation({
|
|
378
|
+
path: targetPath,
|
|
379
|
+
branch,
|
|
380
|
+
baseBranch: request.baseBranch,
|
|
381
|
+
copySessionData: request.copySessionData,
|
|
382
|
+
copyClaudeDirectory: request.copyClaudeDirectory,
|
|
383
|
+
presetId: request.creationMode === 'prompt' ? request.presetId : undefined,
|
|
384
|
+
initialPrompt: request.creationMode === 'prompt'
|
|
385
|
+
? request.initialPrompt
|
|
386
|
+
: undefined,
|
|
387
|
+
ambiguousError: result.left,
|
|
388
|
+
});
|
|
389
|
+
navigateWithClear('remote-branch-selector');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
415
392
|
const errorMessage = formatPreCreationHookError(result.left);
|
|
416
393
|
if (result.left._tag === 'ProcessError') {
|
|
417
394
|
setError(null);
|
|
@@ -473,7 +450,11 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
473
450
|
const result = await Effect.runPromise(Effect.either(worktreeService.createWorktreeEffect(creationData.path, creationData.branch, selectedRemoteRef, // Use the selected remote reference
|
|
474
451
|
creationData.copySessionData, creationData.copyClaudeDirectory)));
|
|
475
452
|
if (result._tag === 'Left') {
|
|
476
|
-
|
|
453
|
+
if (result.left._tag === 'AmbiguousBranchError') {
|
|
454
|
+
setError(result.left.message);
|
|
455
|
+
setView('new-worktree');
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
477
458
|
const errorMessage = formatPreCreationHookError(result.left);
|
|
478
459
|
if (result.left._tag === 'ProcessError') {
|
|
479
460
|
setError(null);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect } from 'effect';
|
|
2
|
-
import { Worktree, CreateWorktreeResult, MergeConfig } from '../types/index.js';
|
|
2
|
+
import { Worktree, CreateWorktreeResult, AmbiguousBranchError, MergeConfig } from '../types/index.js';
|
|
3
3
|
import { GitError, FileSystemError, ProcessError } from '../types/errors.js';
|
|
4
4
|
/**
|
|
5
5
|
* WorktreeService - Git worktree management with Effect-based error handling
|
|
@@ -53,6 +53,7 @@ export declare class WorktreeService {
|
|
|
53
53
|
* @throws {AmbiguousBranchError} When branch exists in multiple remotes
|
|
54
54
|
*/
|
|
55
55
|
private resolveBranchReference;
|
|
56
|
+
private resolveBranchReferenceEffect;
|
|
56
57
|
/**
|
|
57
58
|
* SYNCHRONOUS HELPER: Gets all git remotes for this repository.
|
|
58
59
|
*
|
|
@@ -323,7 +324,7 @@ export declare class WorktreeService {
|
|
|
323
324
|
* @throws {GitError} When git worktree add command fails
|
|
324
325
|
* @throws {FileSystemError} When session data copy fails
|
|
325
326
|
*/
|
|
326
|
-
createWorktreeEffect(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean): Effect.Effect<CreateWorktreeResult, GitError | FileSystemError | ProcessError, never>;
|
|
327
|
+
createWorktreeEffect(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean): Effect.Effect<CreateWorktreeResult, GitError | FileSystemError | ProcessError | AmbiguousBranchError, never>;
|
|
327
328
|
/**
|
|
328
329
|
* Effect-based deleteWorktree operation
|
|
329
330
|
* May fail with GitError
|
|
@@ -162,6 +162,18 @@ export class WorktreeService {
|
|
|
162
162
|
return branchName;
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
|
+
resolveBranchReferenceEffect(branchName) {
|
|
166
|
+
return Effect.try({
|
|
167
|
+
try: () => this.resolveBranchReference(branchName),
|
|
168
|
+
catch: error => {
|
|
169
|
+
if (error instanceof AmbiguousBranchError)
|
|
170
|
+
return error;
|
|
171
|
+
// resolveBranchReference only re-throws AmbiguousBranchError; all
|
|
172
|
+
// other errors are swallowed internally. This path is unreachable.
|
|
173
|
+
throw error;
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
165
177
|
/**
|
|
166
178
|
* SYNCHRONOUS HELPER: Gets all git remotes for this repository.
|
|
167
179
|
*
|
|
@@ -812,12 +824,18 @@ export class WorktreeService {
|
|
|
812
824
|
if (localBranchExists) {
|
|
813
825
|
command = `git worktree add "${resolvedPath}" "${branch}"`;
|
|
814
826
|
}
|
|
827
|
+
else if (baseBranch.endsWith(`/${branch}`)) {
|
|
828
|
+
// baseBranch is already a remote-tracking ref for branch
|
|
829
|
+
// (e.g. "origin/feature/x" after the user resolved an ambiguity).
|
|
830
|
+
// Use it directly to avoid re-triggering AmbiguousBranchError.
|
|
831
|
+
command = `git worktree add -b "${branch}" "${resolvedPath}" "${baseBranch}"`;
|
|
832
|
+
}
|
|
815
833
|
else {
|
|
816
|
-
const resolvedRef = self.
|
|
834
|
+
const resolvedRef = yield* self.resolveBranchReferenceEffect(branch);
|
|
817
835
|
const isRemoteBranch = resolvedRef !== branch;
|
|
818
836
|
const startPoint = isRemoteBranch
|
|
819
837
|
? resolvedRef
|
|
820
|
-
: self.
|
|
838
|
+
: yield* self.resolveBranchReferenceEffect(baseBranch);
|
|
821
839
|
command = `git worktree add -b "${branch}" "${resolvedPath}" "${startPoint}"`;
|
|
822
840
|
}
|
|
823
841
|
// Execute the worktree creation command
|
|
@@ -5,6 +5,7 @@ import { existsSync, statSync } from 'fs';
|
|
|
5
5
|
import { configReader } from './config/configReader.js';
|
|
6
6
|
import { Effect } from 'effect';
|
|
7
7
|
import { GitError, ProcessError } from '../types/errors.js';
|
|
8
|
+
import { AmbiguousBranchError } from '../types/index.js';
|
|
8
9
|
// Mock child_process module
|
|
9
10
|
vi.mock('child_process');
|
|
10
11
|
// Mock fs module
|
|
@@ -369,6 +370,61 @@ origin/feature/test
|
|
|
369
370
|
expect(result).toBe('foo/bar-xyz');
|
|
370
371
|
});
|
|
371
372
|
});
|
|
373
|
+
describe('resolveBranchReferenceEffect', () => {
|
|
374
|
+
it('should succeed with remote ref when single remote has the branch', async () => {
|
|
375
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
376
|
+
if (typeof cmd === 'string') {
|
|
377
|
+
if (cmd.includes('show-ref --verify --quiet refs/heads/foo/bar-xyz')) {
|
|
378
|
+
throw new Error('Local branch not found');
|
|
379
|
+
}
|
|
380
|
+
if (cmd === 'git remote') {
|
|
381
|
+
return 'origin\nupstream\n';
|
|
382
|
+
}
|
|
383
|
+
if (cmd.includes('show-ref --verify --quiet refs/remotes/origin/foo/bar-xyz')) {
|
|
384
|
+
return '';
|
|
385
|
+
}
|
|
386
|
+
if (cmd.includes('show-ref --verify --quiet refs/remotes/upstream/foo/bar-xyz')) {
|
|
387
|
+
throw new Error('Remote branch not found in upstream');
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
391
|
+
});
|
|
392
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
393
|
+
const effect = service.resolveBranchReferenceEffect('foo/bar-xyz');
|
|
394
|
+
const result = await Effect.runPromise(Effect.either(effect));
|
|
395
|
+
expect(result._tag).toBe('Right');
|
|
396
|
+
if (result._tag === 'Right') {
|
|
397
|
+
expect(result.right).toBe('origin/foo/bar-xyz');
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
it('should fail with AmbiguousBranchError (not Die) when multiple remotes have the branch', async () => {
|
|
401
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
402
|
+
if (typeof cmd === 'string') {
|
|
403
|
+
if (cmd.includes('show-ref --verify --quiet refs/heads/foo/bar-xyz')) {
|
|
404
|
+
throw new Error('Local branch not found');
|
|
405
|
+
}
|
|
406
|
+
if (cmd === 'git remote') {
|
|
407
|
+
return 'origin\nupstream\n';
|
|
408
|
+
}
|
|
409
|
+
if (cmd.includes('show-ref --verify --quiet refs/remotes/origin/foo/bar-xyz') ||
|
|
410
|
+
cmd.includes('show-ref --verify --quiet refs/remotes/upstream/foo/bar-xyz')) {
|
|
411
|
+
return '';
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
415
|
+
});
|
|
416
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
417
|
+
const effect = service.resolveBranchReferenceEffect('foo/bar-xyz');
|
|
418
|
+
const result = await Effect.runPromise(Effect.either(effect));
|
|
419
|
+
expect(result._tag).toBe('Left');
|
|
420
|
+
if (result._tag === 'Left') {
|
|
421
|
+
expect(result.left).toBeInstanceOf(AmbiguousBranchError);
|
|
422
|
+
expect(result.left._tag).toBe('AmbiguousBranchError');
|
|
423
|
+
expect(result.left.branchName).toBe('foo/bar-xyz');
|
|
424
|
+
expect(result.left.matches).toHaveLength(2);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
});
|
|
372
428
|
describe('hasClaudeDirectoryInBranchEffect', () => {
|
|
373
429
|
it('should return Effect with true when .claude directory exists in branch worktree', async () => {
|
|
374
430
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
@@ -695,6 +751,65 @@ branch refs/heads/feature
|
|
|
695
751
|
expect(worktreeAddCmd).toContain('-b "feature/remote-only"');
|
|
696
752
|
expect(worktreeAddCmd).toContain('"origin/feature/remote-only"');
|
|
697
753
|
});
|
|
754
|
+
it('should return Effect Left with AmbiguousBranchError when branch exists in multiple remotes', async () => {
|
|
755
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
756
|
+
if (typeof cmd === 'string') {
|
|
757
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
758
|
+
return '/fake/path/.git\n';
|
|
759
|
+
}
|
|
760
|
+
if (cmd.includes('show-ref --verify --quiet refs/heads/')) {
|
|
761
|
+
throw new Error('Branch not found');
|
|
762
|
+
}
|
|
763
|
+
if (cmd === 'git remote') {
|
|
764
|
+
return 'origin\nkbwo-fork\n';
|
|
765
|
+
}
|
|
766
|
+
if (cmd.includes('show-ref --verify --quiet refs/remotes/')) {
|
|
767
|
+
return ''; // Both remotes have the branch
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
771
|
+
});
|
|
772
|
+
const effect = service.createWorktreeEffect('/path/to/worktree', 'feature/feed-mention', 'main');
|
|
773
|
+
const result = await Effect.runPromise(Effect.either(effect));
|
|
774
|
+
expect(result._tag).toBe('Left');
|
|
775
|
+
if (result._tag === 'Left') {
|
|
776
|
+
expect(result.left).toBeInstanceOf(AmbiguousBranchError);
|
|
777
|
+
expect(result.left._tag).toBe('AmbiguousBranchError');
|
|
778
|
+
expect(result.left.branchName).toBe('feature/feed-mention');
|
|
779
|
+
expect(result.left.matches).toHaveLength(2);
|
|
780
|
+
expect(result.left.matches.map(m => m.remote)).toEqual(['origin', 'kbwo-fork']);
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
it('should succeed when baseBranch is already a resolved remote ref for branch (retry after disambiguation)', async () => {
|
|
784
|
+
const executedCommands = [];
|
|
785
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
786
|
+
if (typeof cmd === 'string') {
|
|
787
|
+
executedCommands.push(cmd);
|
|
788
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
789
|
+
return '/fake/path/.git\n';
|
|
790
|
+
}
|
|
791
|
+
// No local branch
|
|
792
|
+
if (cmd.includes('show-ref --verify --quiet refs/heads/')) {
|
|
793
|
+
throw new Error('Branch not found');
|
|
794
|
+
}
|
|
795
|
+
if (cmd.includes('git worktree add')) {
|
|
796
|
+
return '';
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return '';
|
|
800
|
+
});
|
|
801
|
+
// Simulate the retry call: user selected "origin/feature/feed-mention",
|
|
802
|
+
// which is passed as baseBranch.
|
|
803
|
+
const effect = service.createWorktreeEffect('/path/to/worktree', 'feature/feed-mention', 'origin/feature/feed-mention');
|
|
804
|
+
const result = await Effect.runPromise(Effect.either(effect));
|
|
805
|
+
expect(result._tag).toBe('Right');
|
|
806
|
+
// Should use baseBranch directly as startPoint without calling show-ref
|
|
807
|
+
const worktreeAddCmd = executedCommands.find(c => c.includes('git worktree add'));
|
|
808
|
+
expect(worktreeAddCmd).toContain('-b "feature/feed-mention"');
|
|
809
|
+
expect(worktreeAddCmd).toContain('"origin/feature/feed-mention"');
|
|
810
|
+
// show-ref for remotes must NOT be called (no re-resolution)
|
|
811
|
+
expect(executedCommands.some(c => c.includes('show-ref') && c.includes('refs/remotes/'))).toBe(false);
|
|
812
|
+
});
|
|
698
813
|
it('should return Effect that fails with GitError on git command failure', async () => {
|
|
699
814
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
700
815
|
if (typeof cmd === 'string') {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ import type { SerializeAddon } from '@xterm/addon-serialize';
|
|
|
4
4
|
import { GitStatus } from '../utils/gitStatus.js';
|
|
5
5
|
import { Mutex, SessionStateData } from '../utils/mutex.js';
|
|
6
6
|
import type { StateDetector } from '../services/stateDetector/types.js';
|
|
7
|
-
import type {
|
|
7
|
+
import type { Effect } from 'effect';
|
|
8
|
+
import type { GitError, FileSystemError, ProcessError } from './errors.js';
|
|
8
9
|
export type Terminal = InstanceType<typeof pkg.Terminal>;
|
|
9
10
|
export type SessionState = 'idle' | 'busy' | 'waiting_input' | 'pending_auto_approval';
|
|
10
11
|
export type StateDetectionStrategy = 'claude' | 'gemini' | 'codex' | 'cursor' | 'github-copilot' | 'cline' | 'opencode' | 'kimi';
|
|
@@ -249,16 +250,17 @@ export interface RemoteBranchMatch {
|
|
|
249
250
|
fullRef: string;
|
|
250
251
|
}
|
|
251
252
|
export declare class AmbiguousBranchError extends Error {
|
|
253
|
+
readonly _tag: "AmbiguousBranchError";
|
|
252
254
|
branchName: string;
|
|
253
255
|
matches: RemoteBranchMatch[];
|
|
254
256
|
constructor(branchName: string, matches: RemoteBranchMatch[]);
|
|
255
257
|
}
|
|
256
258
|
export interface IWorktreeService {
|
|
257
|
-
getWorktreesEffect():
|
|
259
|
+
getWorktreesEffect(): Effect.Effect<Worktree[], GitError, never>;
|
|
258
260
|
getGitRootPath(): string;
|
|
259
|
-
createWorktreeEffect(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean):
|
|
261
|
+
createWorktreeEffect(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean): Effect.Effect<CreateWorktreeResult, GitError | FileSystemError | ProcessError | AmbiguousBranchError, never>;
|
|
260
262
|
deleteWorktreeEffect(worktreePath: string, options?: {
|
|
261
263
|
deleteBranch?: boolean;
|
|
262
|
-
}):
|
|
263
|
-
mergeWorktreeEffect(sourceBranch: string, targetBranch: string, operation?: 'merge' | 'rebase', mergeConfig?: MergeConfig):
|
|
264
|
+
}): Effect.Effect<void, GitError, never>;
|
|
265
|
+
mergeWorktreeEffect(sourceBranch: string, targetBranch: string, operation?: 'merge' | 'rebase', mergeConfig?: MergeConfig): Effect.Effect<void, GitError, never>;
|
|
264
266
|
}
|
package/dist/types/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.20",
|
|
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": "4.1.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "4.1.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "4.1.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "4.1.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "4.1.
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "4.1.20",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "4.1.20",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "4.1.20",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "4.1.20",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "4.1.20"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|