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
- useInput((input, key) => {
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
- it('pressing 1 calls onSelect with first preset id immediately', () => {
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
- const branchExists = yield* Effect.catchAll(Effect.try({
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 rev-parse --verify ${branch}`, {
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 (branchExists) {
812
+ if (localBranchExists) {
804
813
  command = `git worktree add "${resolvedPath}" "${branch}"`;
805
814
  }
806
815
  else {
807
- // Resolve the base branch to its proper git reference
808
- const resolvedBaseBranch = self.resolveBranchReference(baseBranch);
809
- command = `git worktree add -b "${branch}" "${resolvedPath}" "${resolvedBaseBranch}"`;
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('rev-parse --verify')) {
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('rev-parse --verify')) {
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('rev-parse --verify')) {
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('rev-parse --verify')) {
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.15",
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.15",
45
- "@kodaikabasawa/ccmanager-darwin-x64": "4.1.15",
46
- "@kodaikabasawa/ccmanager-linux-arm64": "4.1.15",
47
- "@kodaikabasawa/ccmanager-linux-x64": "4.1.15",
48
- "@kodaikabasawa/ccmanager-win32-x64": "4.1.15"
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",