ccmanager 0.1.4 → 0.1.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.
@@ -3,6 +3,7 @@ import { Box, Text } from 'ink';
3
3
  import SelectInput from 'ink-select-input';
4
4
  import ConfigureShortcuts from './ConfigureShortcuts.js';
5
5
  import ConfigureHooks from './ConfigureHooks.js';
6
+ import ConfigureWorktree from './ConfigureWorktree.js';
6
7
  const Configuration = ({ onComplete }) => {
7
8
  const [view, setView] = useState('menu');
8
9
  const menuItems = [
@@ -14,6 +15,10 @@ const Configuration = ({ onComplete }) => {
14
15
  label: '🔧 Configure Status Hooks',
15
16
  value: 'hooks',
16
17
  },
18
+ {
19
+ label: '📁 Configure Worktree Settings',
20
+ value: 'worktree',
21
+ },
17
22
  {
18
23
  label: '← Back to Main Menu',
19
24
  value: 'back',
@@ -29,6 +34,9 @@ const Configuration = ({ onComplete }) => {
29
34
  else if (item.value === 'hooks') {
30
35
  setView('hooks');
31
36
  }
37
+ else if (item.value === 'worktree') {
38
+ setView('worktree');
39
+ }
32
40
  };
33
41
  const handleSubMenuComplete = () => {
34
42
  setView('menu');
@@ -39,6 +47,9 @@ const Configuration = ({ onComplete }) => {
39
47
  if (view === 'hooks') {
40
48
  return React.createElement(ConfigureHooks, { onComplete: handleSubMenuComplete });
41
49
  }
50
+ if (view === 'worktree') {
51
+ return React.createElement(ConfigureWorktree, { onComplete: handleSubMenuComplete });
52
+ }
42
53
  return (React.createElement(Box, { flexDirection: "column" },
43
54
  React.createElement(Box, { marginBottom: 1 },
44
55
  React.createElement(Text, { bold: true, color: "green" }, "Configuration")),
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface ConfigureWorktreeProps {
3
+ onComplete: () => void;
4
+ }
5
+ declare const ConfigureWorktree: React.FC<ConfigureWorktreeProps>;
6
+ export default ConfigureWorktree;
@@ -0,0 +1,99 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import SelectInput from 'ink-select-input';
4
+ import TextInput from 'ink-text-input';
5
+ import { configurationManager } from '../services/configurationManager.js';
6
+ import { shortcutManager } from '../services/shortcutManager.js';
7
+ const ConfigureWorktree = ({ onComplete }) => {
8
+ const worktreeConfig = configurationManager.getWorktreeConfig();
9
+ const [autoDirectory, setAutoDirectory] = useState(worktreeConfig.autoDirectory);
10
+ const [pattern, setPattern] = useState(worktreeConfig.autoDirectoryPattern || '../{branch}');
11
+ const [editMode, setEditMode] = useState('menu');
12
+ const [tempPattern, setTempPattern] = useState(pattern);
13
+ useInput((input, key) => {
14
+ if (editMode === 'menu' &&
15
+ shortcutManager.matchesShortcut('cancel', input, key)) {
16
+ onComplete();
17
+ }
18
+ });
19
+ const menuItems = [
20
+ {
21
+ label: `Auto Directory: ${autoDirectory ? '✅ Enabled' : '❌ Disabled'}`,
22
+ value: 'toggle',
23
+ },
24
+ {
25
+ label: `Pattern: ${pattern}`,
26
+ value: 'pattern',
27
+ },
28
+ {
29
+ label: '💾 Save Changes',
30
+ value: 'save',
31
+ },
32
+ {
33
+ label: '← Cancel',
34
+ value: 'cancel',
35
+ },
36
+ ];
37
+ const handleMenuSelect = (item) => {
38
+ switch (item.value) {
39
+ case 'toggle':
40
+ setAutoDirectory(!autoDirectory);
41
+ break;
42
+ case 'pattern':
43
+ setTempPattern(pattern);
44
+ setEditMode('pattern');
45
+ break;
46
+ case 'save':
47
+ // Save the configuration
48
+ configurationManager.setWorktreeConfig({
49
+ autoDirectory,
50
+ autoDirectoryPattern: pattern,
51
+ });
52
+ onComplete();
53
+ break;
54
+ case 'cancel':
55
+ onComplete();
56
+ break;
57
+ }
58
+ };
59
+ const handlePatternSubmit = (value) => {
60
+ if (value.trim()) {
61
+ setPattern(value.trim());
62
+ }
63
+ setEditMode('menu');
64
+ };
65
+ if (editMode === 'pattern') {
66
+ return (React.createElement(Box, { flexDirection: "column" },
67
+ React.createElement(Box, { marginBottom: 1 },
68
+ React.createElement(Text, { bold: true, color: "green" }, "Configure Directory Pattern")),
69
+ React.createElement(Box, { marginBottom: 1 },
70
+ React.createElement(Text, null, "Enter the pattern for automatic directory generation:")),
71
+ React.createElement(Box, { marginBottom: 1 },
72
+ React.createElement(Text, { dimColor: true },
73
+ "Available placeholders: ",
74
+ '{branch}',
75
+ " - full branch name")),
76
+ React.createElement(Box, null,
77
+ React.createElement(Text, { color: "cyan" }, '> '),
78
+ React.createElement(TextInput, { value: tempPattern, onChange: setTempPattern, onSubmit: handlePatternSubmit, placeholder: "../{branch}" })),
79
+ React.createElement(Box, { marginTop: 1 },
80
+ React.createElement(Text, { dimColor: true }, "Press Enter to save or Escape to cancel"))));
81
+ }
82
+ return (React.createElement(Box, { flexDirection: "column" },
83
+ React.createElement(Box, { marginBottom: 1 },
84
+ React.createElement(Text, { bold: true, color: "green" }, "Configure Worktree Settings")),
85
+ React.createElement(Box, { marginBottom: 1 },
86
+ React.createElement(Text, { dimColor: true }, "Configure automatic worktree directory generation")),
87
+ autoDirectory && (React.createElement(Box, { marginBottom: 1 },
88
+ React.createElement(Text, null,
89
+ "Example: branch \"feature/my-feature\" \u2192 directory \"",
90
+ pattern.replace('{branch}', 'feature-my-feature'),
91
+ "\""))),
92
+ React.createElement(SelectInput, { items: menuItems, onSelect: handleMenuSelect, isFocused: true }),
93
+ React.createElement(Box, { marginTop: 1 },
94
+ React.createElement(Text, { dimColor: true },
95
+ "Press ",
96
+ shortcutManager.getShortcutDisplay('cancel'),
97
+ " to cancel without saving"))));
98
+ };
99
+ export default ConfigureWorktree;
@@ -1,11 +1,17 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import TextInput from 'ink-text-input';
4
4
  import { shortcutManager } from '../services/shortcutManager.js';
5
+ import { configurationManager } from '../services/configurationManager.js';
6
+ import { generateWorktreeDirectory } from '../utils/worktreeUtils.js';
5
7
  const NewWorktree = ({ onComplete, onCancel }) => {
6
- const [step, setStep] = useState('path');
8
+ const worktreeConfig = configurationManager.getWorktreeConfig();
9
+ const isAutoDirectory = worktreeConfig.autoDirectory;
10
+ // Adjust initial step based on auto directory mode
11
+ const [step, setStep] = useState(isAutoDirectory ? 'branch' : 'path');
7
12
  const [path, setPath] = useState('');
8
13
  const [branch, setBranch] = useState('');
14
+ const [generatedPath, setGeneratedPath] = useState('');
9
15
  useInput((input, key) => {
10
16
  if (shortcutManager.matchesShortcut('cancel', input, key)) {
11
17
  onCancel();
@@ -20,18 +26,32 @@ const NewWorktree = ({ onComplete, onCancel }) => {
20
26
  const handleBranchSubmit = (value) => {
21
27
  if (value.trim()) {
22
28
  setBranch(value.trim());
23
- onComplete(path, value.trim());
29
+ if (isAutoDirectory) {
30
+ // Generate path from branch name
31
+ const autoPath = generateWorktreeDirectory(value.trim(), worktreeConfig.autoDirectoryPattern);
32
+ onComplete(autoPath, value.trim());
33
+ }
34
+ else {
35
+ onComplete(path, value.trim());
36
+ }
24
37
  }
25
38
  };
39
+ // Update generated path preview when branch changes in auto mode
40
+ useEffect(() => {
41
+ if (isAutoDirectory && branch) {
42
+ const autoPath = generateWorktreeDirectory(branch, worktreeConfig.autoDirectoryPattern);
43
+ setGeneratedPath(autoPath);
44
+ }
45
+ }, [branch, isAutoDirectory, worktreeConfig.autoDirectoryPattern]);
26
46
  return (React.createElement(Box, { flexDirection: "column" },
27
47
  React.createElement(Box, { marginBottom: 1 },
28
48
  React.createElement(Text, { bold: true, color: "green" }, "Create New Worktree")),
29
- step === 'path' ? (React.createElement(Box, { flexDirection: "column" },
49
+ step === 'path' && !isAutoDirectory ? (React.createElement(Box, { flexDirection: "column" },
30
50
  React.createElement(Box, { marginBottom: 1 },
31
51
  React.createElement(Text, null, "Enter worktree path (relative to repository root):")),
32
52
  React.createElement(Box, null,
33
53
  React.createElement(Text, { color: "cyan" }, '> '),
34
- React.createElement(TextInput, { value: path, onChange: setPath, onSubmit: handlePathSubmit, placeholder: "e.g., ../myproject-feature" })))) : (React.createElement(Box, { flexDirection: "column" },
54
+ React.createElement(TextInput, { value: path, onChange: setPath, onSubmit: handlePathSubmit, placeholder: "e.g., ../myproject-feature" })))) : step === 'branch' && !isAutoDirectory ? (React.createElement(Box, { flexDirection: "column" },
35
55
  React.createElement(Box, { marginBottom: 1 },
36
56
  React.createElement(Text, null,
37
57
  "Enter branch name for worktree at ",
@@ -39,7 +59,17 @@ const NewWorktree = ({ onComplete, onCancel }) => {
39
59
  ":")),
40
60
  React.createElement(Box, null,
41
61
  React.createElement(Text, { color: "cyan" }, '> '),
42
- React.createElement(TextInput, { value: branch, onChange: setBranch, onSubmit: handleBranchSubmit, placeholder: "e.g., feature/new-feature" })))),
62
+ React.createElement(TextInput, { value: branch, onChange: setBranch, onSubmit: handleBranchSubmit, placeholder: "e.g., feature/new-feature" })))) : (React.createElement(Box, { flexDirection: "column" },
63
+ React.createElement(Box, { marginBottom: 1 },
64
+ React.createElement(Text, null, "Enter branch name (directory will be auto-generated):")),
65
+ React.createElement(Box, null,
66
+ React.createElement(Text, { color: "cyan" }, '> '),
67
+ React.createElement(TextInput, { value: branch, onChange: setBranch, onSubmit: handleBranchSubmit, placeholder: "e.g., feature/new-feature" })),
68
+ generatedPath && (React.createElement(Box, { marginTop: 1 },
69
+ React.createElement(Text, { dimColor: true },
70
+ "Worktree will be created at:",
71
+ ' ',
72
+ React.createElement(Text, { color: "green" }, generatedPath)))))),
43
73
  React.createElement(Box, { marginTop: 1 },
44
74
  React.createElement(Text, { dimColor: true },
45
75
  "Press ",
@@ -1,4 +1,4 @@
1
- import { ConfigurationData, StatusHookConfig, ShortcutConfig } from '../types/index.js';
1
+ import { ConfigurationData, StatusHookConfig, ShortcutConfig, WorktreeConfig } from '../types/index.js';
2
2
  export declare class ConfigurationManager {
3
3
  private configPath;
4
4
  private legacyShortcutsPath;
@@ -13,5 +13,7 @@ export declare class ConfigurationManager {
13
13
  setStatusHooks(hooks: StatusHookConfig): void;
14
14
  getConfiguration(): ConfigurationData;
15
15
  setConfiguration(config: ConfigurationData): void;
16
+ getWorktreeConfig(): WorktreeConfig;
17
+ setWorktreeConfig(worktreeConfig: WorktreeConfig): void;
16
18
  }
17
19
  export declare const configurationManager: ConfigurationManager;
@@ -63,6 +63,11 @@ export class ConfigurationManager {
63
63
  if (!this.config.statusHooks) {
64
64
  this.config.statusHooks = {};
65
65
  }
66
+ if (!this.config.worktree) {
67
+ this.config.worktree = {
68
+ autoDirectory: false,
69
+ };
70
+ }
66
71
  }
67
72
  migrateLegacyShortcuts() {
68
73
  if (existsSync(this.legacyShortcutsPath)) {
@@ -111,5 +116,14 @@ export class ConfigurationManager {
111
116
  this.config = config;
112
117
  this.saveConfig();
113
118
  }
119
+ getWorktreeConfig() {
120
+ return (this.config.worktree || {
121
+ autoDirectory: false,
122
+ });
123
+ }
124
+ setWorktreeConfig(worktreeConfig) {
125
+ this.config.worktree = worktreeConfig;
126
+ this.saveConfig();
127
+ }
114
128
  }
115
129
  export const configurationManager = new ConfigurationManager();
@@ -179,6 +179,11 @@ export class WorktreeService {
179
179
  cwd: sourceWorktree.path,
180
180
  encoding: 'utf8',
181
181
  });
182
+ // After rebase, merge the rebased source branch into target branch
183
+ execSync(`git merge --ff-only "${sourceBranch}"`, {
184
+ cwd: targetWorktree.path,
185
+ encoding: 'utf8',
186
+ });
182
187
  }
183
188
  else {
184
189
  // Regular merge
@@ -47,13 +47,12 @@ export interface StatusHookConfig {
47
47
  busy?: StatusHook;
48
48
  waiting_input?: StatusHook;
49
49
  }
50
- export interface TerminalPadding {
51
- top: number;
52
- bottom: number;
50
+ export interface WorktreeConfig {
51
+ autoDirectory: boolean;
52
+ autoDirectoryPattern?: string;
53
53
  }
54
- export declare const DEFAULT_TERMINAL_PADDING: TerminalPadding;
55
54
  export interface ConfigurationData {
56
55
  shortcuts?: ShortcutConfig;
57
56
  statusHooks?: StatusHookConfig;
58
- terminalPadding?: TerminalPadding;
57
+ worktree?: WorktreeConfig;
59
58
  }
@@ -2,7 +2,3 @@ export const DEFAULT_SHORTCUTS = {
2
2
  returnToMenu: { ctrl: true, key: 'e' },
3
3
  cancel: { key: 'escape' },
4
4
  };
5
- export const DEFAULT_TERMINAL_PADDING = {
6
- top: 1,
7
- bottom: 1,
8
- };
@@ -0,0 +1,5 @@
1
+ export declare function generateWorktreeDirectory(branchName: string, pattern?: string): string;
2
+ export declare function extractBranchParts(branchName: string): {
3
+ prefix?: string;
4
+ name: string;
5
+ };
@@ -0,0 +1,29 @@
1
+ import path from 'path';
2
+ export function generateWorktreeDirectory(branchName, pattern) {
3
+ // Default pattern if not specified
4
+ const defaultPattern = '../{branch}';
5
+ const activePattern = pattern || defaultPattern;
6
+ // Sanitize branch name for filesystem
7
+ // Replace slashes with dashes, remove special characters
8
+ const sanitizedBranch = branchName
9
+ .replace(/\//g, '-') // Replace forward slashes with dashes
10
+ .replace(/[^a-zA-Z0-9-_.]/g, '') // Remove special characters except dash, dot, underscore
11
+ .replace(/^-+|-+$/g, '') // Remove leading/trailing dashes
12
+ .toLowerCase(); // Convert to lowercase for consistency
13
+ // Replace placeholders in pattern
14
+ const directory = activePattern
15
+ .replace('{branch}', sanitizedBranch)
16
+ .replace('{branch-name}', sanitizedBranch);
17
+ // Ensure the path is relative to the repository root
18
+ return path.normalize(directory);
19
+ }
20
+ export function extractBranchParts(branchName) {
21
+ const parts = branchName.split('/');
22
+ if (parts.length > 1) {
23
+ return {
24
+ prefix: parts[0],
25
+ name: parts.slice(1).join('/'),
26
+ };
27
+ }
28
+ return { name: branchName };
29
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { generateWorktreeDirectory, extractBranchParts, } from './worktreeUtils.js';
3
+ describe('generateWorktreeDirectory', () => {
4
+ describe('with default pattern', () => {
5
+ it('should generate directory with sanitized branch name', () => {
6
+ expect(generateWorktreeDirectory('feature/my-feature')).toBe('../feature-my-feature');
7
+ expect(generateWorktreeDirectory('bugfix/fix-123')).toBe('../bugfix-fix-123');
8
+ expect(generateWorktreeDirectory('release/v1.0.0')).toBe('../release-v1.0.0');
9
+ });
10
+ it('should handle branch names without slashes', () => {
11
+ expect(generateWorktreeDirectory('main')).toBe('../main');
12
+ expect(generateWorktreeDirectory('develop')).toBe('../develop');
13
+ expect(generateWorktreeDirectory('my-feature')).toBe('../my-feature');
14
+ });
15
+ it('should remove special characters', () => {
16
+ expect(generateWorktreeDirectory('feature/my@feature!')).toBe('../feature-myfeature');
17
+ expect(generateWorktreeDirectory('bugfix/#123')).toBe('../bugfix-123');
18
+ expect(generateWorktreeDirectory('release/v1.0.0-beta')).toBe('../release-v1.0.0-beta');
19
+ });
20
+ it('should handle edge cases', () => {
21
+ expect(generateWorktreeDirectory('//feature//')).toBe('../feature');
22
+ expect(generateWorktreeDirectory('-feature-')).toBe('../feature');
23
+ expect(generateWorktreeDirectory('FEATURE/UPPERCASE')).toBe('../feature-uppercase');
24
+ });
25
+ });
26
+ describe('with custom patterns', () => {
27
+ it('should use custom pattern with {branch} placeholder', () => {
28
+ expect(generateWorktreeDirectory('feature/my-feature', '../worktrees/{branch}')).toBe('../worktrees/feature-my-feature');
29
+ expect(generateWorktreeDirectory('bugfix/123', '/tmp/{branch}-wt')).toBe('/tmp/bugfix-123-wt');
30
+ });
31
+ it('should handle patterns without placeholders', () => {
32
+ expect(generateWorktreeDirectory('feature/test', '../fixed-directory')).toBe('../fixed-directory');
33
+ });
34
+ it('should normalize paths', () => {
35
+ expect(generateWorktreeDirectory('feature/test', '../foo/../bar/{branch}')).toBe('../bar/feature-test');
36
+ expect(generateWorktreeDirectory('feature/test', './worktrees/{branch}')).toBe('worktrees/feature-test');
37
+ });
38
+ });
39
+ });
40
+ describe('extractBranchParts', () => {
41
+ it('should extract prefix and name from branch with slash', () => {
42
+ expect(extractBranchParts('feature/my-feature')).toEqual({
43
+ prefix: 'feature',
44
+ name: 'my-feature',
45
+ });
46
+ expect(extractBranchParts('bugfix/fix-123')).toEqual({
47
+ prefix: 'bugfix',
48
+ name: 'fix-123',
49
+ });
50
+ });
51
+ it('should handle branches with multiple slashes', () => {
52
+ expect(extractBranchParts('feature/user/profile-page')).toEqual({
53
+ prefix: 'feature',
54
+ name: 'user/profile-page',
55
+ });
56
+ expect(extractBranchParts('release/v1.0/final')).toEqual({
57
+ prefix: 'release',
58
+ name: 'v1.0/final',
59
+ });
60
+ });
61
+ it('should handle branches without slashes', () => {
62
+ expect(extractBranchParts('main')).toEqual({
63
+ name: 'main',
64
+ });
65
+ expect(extractBranchParts('develop')).toEqual({
66
+ name: 'develop',
67
+ });
68
+ });
69
+ it('should handle empty branch name', () => {
70
+ expect(extractBranchParts('')).toEqual({
71
+ name: '',
72
+ });
73
+ });
74
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",