ccmanager 4.1.15 → 4.1.17
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.
|
@@ -37,18 +37,11 @@ const PresetSelector = ({ onSelect, onCancel, }) => {
|
|
|
37
37
|
};
|
|
38
38
|
// Find initial index based on default preset
|
|
39
39
|
const initialIndex = selectItems.findIndex(item => item.value === defaultPresetId);
|
|
40
|
-
|
|
40
|
+
// ink-select-input v6+ handles number keys 1-9 natively, so only handle ESC here
|
|
41
|
+
// to avoid double-firing onSelect (which would create two sessions for one worktree).
|
|
42
|
+
useInput((_input, key) => {
|
|
41
43
|
if (key.escape) {
|
|
42
44
|
onCancel();
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
// Number keys 1-9: immediate launch
|
|
46
|
-
if (/^[1-9]$/.test(input)) {
|
|
47
|
-
const idx = parseInt(input) - 1;
|
|
48
|
-
if (idx < presets.length && presets[idx]) {
|
|
49
|
-
onSelect(presets[idx].id);
|
|
50
|
-
}
|
|
51
|
-
return;
|
|
52
45
|
}
|
|
53
46
|
});
|
|
54
47
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "green", children: "Select Command Preset" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Choose a preset to start the session with" }) }), _jsx(SelectInput, { items: selectItems, onSelect: handleSelectItem, initialIndex: initialIndex >= 0 ? initialIndex : 0 }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191\u2193 Navigate 1-9 Quick Select Enter Select ESC Cancel" }) })] }));
|
|
@@ -86,26 +86,15 @@ describe('PresetSelector component', () => {
|
|
|
86
86
|
expect(output).toContain('(default)');
|
|
87
87
|
expect(output).toContain('← Cancel');
|
|
88
88
|
});
|
|
89
|
-
|
|
89
|
+
// Number key selection is handled by ink-select-input v6+ natively.
|
|
90
|
+
// PresetSelector's useInput only handles ESC to avoid double-firing onSelect
|
|
91
|
+
// (which would create two sessions for one worktree).
|
|
92
|
+
it('pressing number keys via useInput does NOT trigger onSelect', () => {
|
|
90
93
|
render(_jsx(PresetSelector, { onSelect: onSelect, onCancel: onCancel }));
|
|
91
94
|
expect(capturedHandlers.inputHandler).not.toBeNull();
|
|
92
95
|
capturedHandlers.inputHandler('1', makeKey());
|
|
93
|
-
expect(onSelect).toHaveBeenCalledWith('preset-1');
|
|
94
|
-
expect(onCancel).not.toHaveBeenCalled();
|
|
95
|
-
});
|
|
96
|
-
it('pressing 2 calls onSelect with second preset id immediately', () => {
|
|
97
|
-
render(_jsx(PresetSelector, { onSelect: onSelect, onCancel: onCancel }));
|
|
98
96
|
capturedHandlers.inputHandler('2', makeKey());
|
|
99
|
-
expect(onSelect).toHaveBeenCalledWith('preset-2');
|
|
100
|
-
});
|
|
101
|
-
it('pressing 3 calls onSelect with third preset id immediately', () => {
|
|
102
|
-
render(_jsx(PresetSelector, { onSelect: onSelect, onCancel: onCancel }));
|
|
103
97
|
capturedHandlers.inputHandler('3', makeKey());
|
|
104
|
-
expect(onSelect).toHaveBeenCalledWith('preset-3');
|
|
105
|
-
});
|
|
106
|
-
it('pressing a number beyond preset count does nothing', () => {
|
|
107
|
-
render(_jsx(PresetSelector, { onSelect: onSelect, onCancel: onCancel }));
|
|
108
|
-
capturedHandlers.inputHandler('9', makeKey());
|
|
109
98
|
expect(onSelect).not.toHaveBeenCalled();
|
|
110
99
|
expect(onCancel).not.toHaveBeenCalled();
|
|
111
100
|
});
|
|
@@ -775,10 +775,14 @@ export class WorktreeService {
|
|
|
775
775
|
copySessionData,
|
|
776
776
|
copyClaudeDirectory,
|
|
777
777
|
});
|
|
778
|
-
// Check if branch exists
|
|
779
|
-
|
|
778
|
+
// Check if a LOCAL branch exists (refs/heads/ only).
|
|
779
|
+
// Using `git rev-parse --verify` without qualifying the ref would also
|
|
780
|
+
// match remote-tracking refs (e.g. refs/remotes/origin/<branch>), which
|
|
781
|
+
// causes `git worktree add` to create the worktree in detached-HEAD
|
|
782
|
+
// state instead of on a proper local branch.
|
|
783
|
+
const localBranchExists = yield* Effect.catchAll(Effect.try({
|
|
780
784
|
try: () => {
|
|
781
|
-
execSync(`git
|
|
785
|
+
execSync(`git show-ref --verify --quiet refs/heads/${branch}`, {
|
|
782
786
|
cwd: self.rootPath,
|
|
783
787
|
encoding: 'utf8',
|
|
784
788
|
});
|
|
@@ -798,15 +802,23 @@ export class WorktreeService {
|
|
|
798
802
|
worktreeHooksConfig.pre_creation?.command) {
|
|
799
803
|
yield* executeWorktreePreCreationHook(worktreeHooksConfig.pre_creation.command, resolvedPath, branch, absoluteGitRoot, baseBranch);
|
|
800
804
|
}
|
|
801
|
-
// Create the worktree command
|
|
805
|
+
// Create the worktree command.
|
|
806
|
+
// Three cases:
|
|
807
|
+
// 1. Local branch exists → attach worktree directly
|
|
808
|
+
// 2. No local branch, but remote-tracking branch exists → create
|
|
809
|
+
// local branch from the remote ref (avoids detached HEAD)
|
|
810
|
+
// 3. Branch is brand-new → create from baseBranch
|
|
802
811
|
let command;
|
|
803
|
-
if (
|
|
812
|
+
if (localBranchExists) {
|
|
804
813
|
command = `git worktree add "${resolvedPath}" "${branch}"`;
|
|
805
814
|
}
|
|
806
815
|
else {
|
|
807
|
-
|
|
808
|
-
const
|
|
809
|
-
|
|
816
|
+
const resolvedRef = self.resolveBranchReference(branch);
|
|
817
|
+
const isRemoteBranch = resolvedRef !== branch;
|
|
818
|
+
const startPoint = isRemoteBranch
|
|
819
|
+
? resolvedRef
|
|
820
|
+
: self.resolveBranchReference(baseBranch);
|
|
821
|
+
command = `git worktree add -b "${branch}" "${resolvedPath}" "${startPoint}"`;
|
|
810
822
|
}
|
|
811
823
|
// Execute the worktree creation command
|
|
812
824
|
yield* Effect.try({
|
|
@@ -631,9 +631,15 @@ branch refs/heads/feature
|
|
|
631
631
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
632
632
|
return '/fake/path/.git\n';
|
|
633
633
|
}
|
|
634
|
-
if (cmd.includes('
|
|
634
|
+
if (cmd.includes('show-ref --verify --quiet refs/heads/')) {
|
|
635
635
|
throw new Error('Branch not found');
|
|
636
636
|
}
|
|
637
|
+
if (cmd === 'git remote') {
|
|
638
|
+
return 'origin\n';
|
|
639
|
+
}
|
|
640
|
+
if (cmd.includes('show-ref --verify --quiet refs/remotes/')) {
|
|
641
|
+
throw new Error('Remote branch not found');
|
|
642
|
+
}
|
|
637
643
|
if (cmd.includes('git worktree add')) {
|
|
638
644
|
return '';
|
|
639
645
|
}
|
|
@@ -648,15 +654,62 @@ branch refs/heads/feature
|
|
|
648
654
|
isMainWorktree: false,
|
|
649
655
|
});
|
|
650
656
|
});
|
|
657
|
+
it('should create local branch from remote ref when only remote branch exists', async () => {
|
|
658
|
+
const executedCommands = [];
|
|
659
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
660
|
+
if (typeof cmd === 'string') {
|
|
661
|
+
executedCommands.push(cmd);
|
|
662
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
663
|
+
return '/fake/path/.git\n';
|
|
664
|
+
}
|
|
665
|
+
// No local branch
|
|
666
|
+
if (cmd.includes('show-ref --verify --quiet refs/heads/')) {
|
|
667
|
+
throw new Error('Branch not found');
|
|
668
|
+
}
|
|
669
|
+
if (cmd === 'git remote') {
|
|
670
|
+
return 'origin\n';
|
|
671
|
+
}
|
|
672
|
+
// Remote branch exists
|
|
673
|
+
if (cmd ===
|
|
674
|
+
'git show-ref --verify --quiet refs/remotes/origin/feature/remote-only') {
|
|
675
|
+
return '';
|
|
676
|
+
}
|
|
677
|
+
if (cmd.includes('show-ref --verify --quiet refs/remotes/')) {
|
|
678
|
+
throw new Error('Remote branch not found');
|
|
679
|
+
}
|
|
680
|
+
if (cmd.includes('git worktree add')) {
|
|
681
|
+
return '';
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return '';
|
|
685
|
+
});
|
|
686
|
+
const effect = service.createWorktreeEffect('/path/to/worktree', 'feature/remote-only', 'main');
|
|
687
|
+
const result = await Effect.runPromise(effect);
|
|
688
|
+
expect(result.worktree).toMatchObject({
|
|
689
|
+
path: '/path/to/worktree',
|
|
690
|
+
branch: 'feature/remote-only',
|
|
691
|
+
isMainWorktree: false,
|
|
692
|
+
});
|
|
693
|
+
// Should use -b with the remote ref as start point, not baseBranch
|
|
694
|
+
const worktreeAddCmd = executedCommands.find(c => c.includes('git worktree add'));
|
|
695
|
+
expect(worktreeAddCmd).toContain('-b "feature/remote-only"');
|
|
696
|
+
expect(worktreeAddCmd).toContain('"origin/feature/remote-only"');
|
|
697
|
+
});
|
|
651
698
|
it('should return Effect that fails with GitError on git command failure', async () => {
|
|
652
699
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
653
700
|
if (typeof cmd === 'string') {
|
|
654
701
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
655
702
|
return '/fake/path/.git\n';
|
|
656
703
|
}
|
|
657
|
-
if (cmd.includes('
|
|
704
|
+
if (cmd.includes('show-ref --verify --quiet refs/heads/')) {
|
|
658
705
|
throw new Error('Branch not found');
|
|
659
706
|
}
|
|
707
|
+
if (cmd === 'git remote') {
|
|
708
|
+
return 'origin\n';
|
|
709
|
+
}
|
|
710
|
+
if (cmd.includes('show-ref --verify --quiet refs/remotes/')) {
|
|
711
|
+
throw new Error('Remote branch not found');
|
|
712
|
+
}
|
|
660
713
|
if (cmd.includes('git worktree add')) {
|
|
661
714
|
const error = new Error('fatal: invalid reference: main');
|
|
662
715
|
error.status = 128;
|
|
@@ -696,9 +749,15 @@ branch refs/heads/feature
|
|
|
696
749
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
697
750
|
return '/fake/path/.git\n';
|
|
698
751
|
}
|
|
699
|
-
if (cmd.includes('
|
|
752
|
+
if (cmd.includes('show-ref --verify --quiet refs/heads/')) {
|
|
700
753
|
throw new Error('Branch not found');
|
|
701
754
|
}
|
|
755
|
+
if (cmd === 'git remote') {
|
|
756
|
+
return 'origin\n';
|
|
757
|
+
}
|
|
758
|
+
if (cmd.includes('show-ref --verify --quiet refs/remotes/')) {
|
|
759
|
+
throw new Error('Remote branch not found');
|
|
760
|
+
}
|
|
702
761
|
if (cmd.includes('git worktree add')) {
|
|
703
762
|
throw new Error('git worktree add should not be called');
|
|
704
763
|
}
|
|
@@ -732,9 +791,15 @@ branch refs/heads/feature
|
|
|
732
791
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
733
792
|
return '/fake/path/.git\n';
|
|
734
793
|
}
|
|
735
|
-
if (cmd.includes('
|
|
794
|
+
if (cmd.includes('show-ref --verify --quiet refs/heads/')) {
|
|
736
795
|
throw new Error('Branch not found');
|
|
737
796
|
}
|
|
797
|
+
if (cmd === 'git remote') {
|
|
798
|
+
return 'origin\n';
|
|
799
|
+
}
|
|
800
|
+
if (cmd.includes('show-ref --verify --quiet refs/remotes/')) {
|
|
801
|
+
throw new Error('Remote branch not found');
|
|
802
|
+
}
|
|
738
803
|
if (cmd.includes('git worktree add')) {
|
|
739
804
|
return '';
|
|
740
805
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.17",
|
|
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.17",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "4.1.17",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "4.1.17",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "4.1.17",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "4.1.17"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|