ccmanager 3.8.1 โ†’ 3.9.0

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.
@@ -7,6 +7,7 @@ import ConfigureStatusHooks from './ConfigureStatusHooks.js';
7
7
  import ConfigureWorktreeHooks from './ConfigureWorktreeHooks.js';
8
8
  import ConfigureWorktree from './ConfigureWorktree.js';
9
9
  import ConfigureCommand from './ConfigureCommand.js';
10
+ import ConfigureMerge from './ConfigureMerge.js';
10
11
  import ConfigureOther from './ConfigureOther.js';
11
12
  import { shortcutManager } from '../services/shortcutManager.js';
12
13
  import { ConfigEditorProvider } from '../contexts/ConfigEditorContext.js';
@@ -34,6 +35,10 @@ const ConfigurationContent = ({ scope, onComplete }) => {
34
35
  label: 'C ๐Ÿš€ Configure Command Presets',
35
36
  value: 'presets',
36
37
  },
38
+ {
39
+ label: 'M ๐Ÿ”€ Configure Merge/Rebase',
40
+ value: 'mergeConfig',
41
+ },
37
42
  {
38
43
  label: 'O ๐Ÿงช Other & Experimental',
39
44
  value: 'other',
@@ -62,6 +67,9 @@ const ConfigurationContent = ({ scope, onComplete }) => {
62
67
  else if (item.value === 'presets') {
63
68
  setView('presets');
64
69
  }
70
+ else if (item.value === 'mergeConfig') {
71
+ setView('mergeConfig');
72
+ }
65
73
  else if (item.value === 'other') {
66
74
  setView('other');
67
75
  }
@@ -90,6 +98,9 @@ const ConfigurationContent = ({ scope, onComplete }) => {
90
98
  case 'c':
91
99
  setView('presets');
92
100
  break;
101
+ case 'm':
102
+ setView('mergeConfig');
103
+ break;
93
104
  case 'o':
94
105
  setView('other');
95
106
  break;
@@ -117,6 +128,9 @@ const ConfigurationContent = ({ scope, onComplete }) => {
117
128
  if (view === 'presets') {
118
129
  return _jsx(ConfigureCommand, { onComplete: handleSubMenuComplete });
119
130
  }
131
+ if (view === 'mergeConfig') {
132
+ return _jsx(ConfigureMerge, { onComplete: handleSubMenuComplete });
133
+ }
120
134
  if (view === 'other') {
121
135
  return _jsx(ConfigureOther, { onComplete: handleSubMenuComplete });
122
136
  }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface ConfigureMergeProps {
3
+ onComplete: () => void;
4
+ }
5
+ declare const ConfigureMerge: React.FC<ConfigureMergeProps>;
6
+ export default ConfigureMerge;
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import SelectInput from 'ink-select-input';
5
+ import TextInputWrapper from './TextInputWrapper.js';
6
+ import { useConfigEditor } from '../contexts/ConfigEditorContext.js';
7
+ import { shortcutManager } from '../services/shortcutManager.js';
8
+ const DEFAULT_MERGE_ARGS = ['--no-ff'];
9
+ const DEFAULT_REBASE_ARGS = [];
10
+ const ConfigureMerge = ({ onComplete }) => {
11
+ const configEditor = useConfigEditor();
12
+ const scope = configEditor.getScope();
13
+ const currentConfig = configEditor.getMergeConfig() || {};
14
+ const [mergeConfig, setMergeConfig] = useState(currentConfig);
15
+ const [editField, setEditField] = useState(null);
16
+ const [inputValue, setInputValue] = useState('');
17
+ const isInheriting = scope === 'project' && !configEditor.hasProjectOverride('mergeConfig');
18
+ const getMergeArgs = () => mergeConfig.mergeArgs ?? DEFAULT_MERGE_ARGS;
19
+ const getRebaseArgs = () => mergeConfig.rebaseArgs ?? DEFAULT_REBASE_ARGS;
20
+ const formatArgs = (args) => args.length > 0 ? args.join(' ') : '(none)';
21
+ const menuItems = [
22
+ {
23
+ label: `Merge Arguments: ${formatArgs(getMergeArgs())}`,
24
+ value: 'mergeArgs',
25
+ },
26
+ {
27
+ label: `Rebase Arguments: ${formatArgs(getRebaseArgs())}`,
28
+ value: 'rebaseArgs',
29
+ },
30
+ { label: '-----', value: 'separator' },
31
+ { label: '<- Back', value: 'back' },
32
+ ];
33
+ const handleSelect = (item) => {
34
+ if (item.value === 'separator')
35
+ return;
36
+ if (item.value === 'back') {
37
+ onComplete();
38
+ return;
39
+ }
40
+ const field = item.value;
41
+ setEditField(field);
42
+ switch (field) {
43
+ case 'mergeArgs':
44
+ setInputValue(getMergeArgs().join(' '));
45
+ break;
46
+ case 'rebaseArgs':
47
+ setInputValue(getRebaseArgs().join(' '));
48
+ break;
49
+ }
50
+ };
51
+ const handleFieldUpdate = (value) => {
52
+ const args = value.trim() ? value.trim().split(/\s+/) : [];
53
+ const updated = { ...mergeConfig, [editField]: args };
54
+ setMergeConfig(updated);
55
+ configEditor.setMergeConfig(updated);
56
+ setEditField(null);
57
+ setInputValue('');
58
+ };
59
+ useInput((input, key) => {
60
+ if (shortcutManager.matchesShortcut('cancel', input, key)) {
61
+ if (editField) {
62
+ setEditField(null);
63
+ setInputValue('');
64
+ }
65
+ else {
66
+ onComplete();
67
+ }
68
+ return;
69
+ }
70
+ });
71
+ if (editField) {
72
+ const titles = {
73
+ mergeArgs: 'Enter merge arguments (space-separated, default: --no-ff):',
74
+ rebaseArgs: 'Enter rebase arguments (space-separated, default: none):',
75
+ };
76
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "green", children: "Edit Merge Config" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: titles[editField] }) }), _jsx(Box, { children: _jsx(TextInputWrapper, { value: inputValue, onChange: setInputValue, onSubmit: handleFieldUpdate, placeholder: "e.g., --no-ff or leave empty" }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Press Enter to save, ", shortcutManager.getShortcutDisplay('cancel'), ' ', "to cancel"] }) })] }));
77
+ }
78
+ const scopeLabel = scope === 'project' ? 'Project' : 'Global';
79
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "green", children: ["Configure Merge/Rebase (", scopeLabel, ")"] }) }), isInheriting && (_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { backgroundColor: "cyan", color: "black", children: [' ', "Inheriting from global configuration", ' '] }) })), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Configure arguments for merge and rebase operations" }) }), _jsx(SelectInput, { items: menuItems, onSelect: handleSelect }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Press Enter to edit, ", shortcutManager.getShortcutDisplay('cancel'), " to go back"] }) })] }));
80
+ };
81
+ export default ConfigureMerge;
@@ -4,6 +4,7 @@ import { Box, Text, useInput } from 'ink';
4
4
  import SelectInput from 'ink-select-input';
5
5
  import { Effect } from 'effect';
6
6
  import { WorktreeService } from '../services/worktreeService.js';
7
+ import { configReader } from '../services/config/configReader.js';
7
8
  import Confirmation, { SimpleConfirmation } from './Confirmation.js';
8
9
  import { shortcutManager } from '../services/shortcutManager.js';
9
10
  import { GitError } from '../types/errors.js';
@@ -14,9 +15,10 @@ const MergeWorktree = ({ onComplete, onCancel, }) => {
14
15
  const [targetBranch, setTargetBranch] = useState('');
15
16
  const [branchItems, setBranchItems] = useState([]);
16
17
  const [originalBranchItems, setOriginalBranchItems] = useState([]);
17
- const [useRebase, setUseRebase] = useState(false);
18
+ const [operation, setOperation] = useState('merge');
18
19
  const [mergeError, setMergeError] = useState(null);
19
20
  const [worktreeService] = useState(() => new WorktreeService());
21
+ const [mergeConfig] = useState(() => configReader.getMergeConfig());
20
22
  const [isLoading, setIsLoading] = useState(true);
21
23
  const [loadError, setLoadError] = useState(null);
22
24
  useEffect(() => {
@@ -83,7 +85,7 @@ const MergeWorktree = ({ onComplete, onCancel, }) => {
83
85
  return;
84
86
  const performMerge = async () => {
85
87
  try {
86
- await Effect.runPromise(worktreeService.mergeWorktreeEffect(sourceBranch, targetBranch, useRebase));
88
+ await Effect.runPromise(worktreeService.mergeWorktreeEffect(sourceBranch, targetBranch, operation, mergeConfig));
87
89
  // Merge successful, check for uncommitted changes before asking about deletion
88
90
  setStep('check-uncommitted');
89
91
  }
@@ -99,7 +101,14 @@ const MergeWorktree = ({ onComplete, onCancel, }) => {
99
101
  }
100
102
  };
101
103
  performMerge();
102
- }, [step, sourceBranch, targetBranch, useRebase, worktreeService]);
104
+ }, [
105
+ step,
106
+ sourceBranch,
107
+ targetBranch,
108
+ operation,
109
+ mergeConfig,
110
+ worktreeService,
111
+ ]);
103
112
  // Check for uncommitted changes in source worktree when entering check-uncommitted step
104
113
  useEffect(() => {
105
114
  if (step !== 'check-uncommitted')
@@ -140,7 +149,7 @@ const MergeWorktree = ({ onComplete, onCancel, }) => {
140
149
  const message = (_jsxs(Text, { children: ["Choose how to integrate ", _jsx(Text, { color: "yellow", children: sourceBranch }), " into", ' ', _jsx(Text, { color: "yellow", children: targetBranch }), ":"] }));
141
150
  const hint = (_jsxs(Text, { dimColor: true, children: ["Use \u2191\u2193/j/k to navigate, Enter to select,", ' ', shortcutManager.getShortcutDisplay('cancel'), " to cancel"] }));
142
151
  const handleOperationSelect = (value) => {
143
- setUseRebase(value === 'rebase');
152
+ setOperation(value);
144
153
  setStep('confirm-merge');
145
154
  };
146
155
  return (_jsx(Confirmation, { title: title, message: message, options: [
@@ -149,14 +158,18 @@ const MergeWorktree = ({ onComplete, onCancel, }) => {
149
158
  ], onSelect: handleOperationSelect, initialIndex: 0, hint: hint }));
150
159
  }
151
160
  if (step === 'confirm-merge') {
152
- const confirmMessage = (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "green", children: ["Confirm ", useRebase ? 'Rebase' : 'Merge'] }) }), _jsxs(Text, { children: [useRebase ? 'Rebase' : 'Merge', ' ', _jsx(Text, { color: "yellow", children: sourceBranch }), ' ', useRebase ? 'onto' : 'into', ' ', _jsx(Text, { color: "yellow", children: targetBranch }), "?"] })] }));
161
+ const operationLabel = operation === 'rebase' ? 'Rebase' : 'Merge';
162
+ const preposition = operation === 'rebase' ? 'onto' : 'into';
163
+ const confirmMessage = (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "green", children: ["Confirm ", operationLabel] }) }), _jsxs(Text, { children: [operationLabel, " ", _jsx(Text, { color: "yellow", children: sourceBranch }), ' ', preposition, " ", _jsx(Text, { color: "yellow", children: targetBranch }), "?"] })] }));
153
164
  return (_jsx(SimpleConfirmation, { message: confirmMessage, onConfirm: () => setStep('executing-merge'), onCancel: onCancel }));
154
165
  }
155
166
  if (step === 'executing-merge') {
156
- return (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { color: "green", children: [useRebase ? 'Rebasing' : 'Merging', " branches..."] }) }));
167
+ const executingLabel = operation === 'rebase' ? 'Rebasing' : 'Merging';
168
+ return (_jsx(Box, { flexDirection: "column", children: _jsxs(Text, { color: "green", children: [executingLabel, " branches..."] }) }));
157
169
  }
158
170
  if (step === 'merge-error') {
159
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "red", children: [useRebase ? 'Rebase' : 'Merge', " Failed"] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "red", children: mergeError }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press any key to return to menu" }) })] }));
171
+ const errorLabel = operation === 'rebase' ? 'Rebase' : 'Merge';
172
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, color: "red", children: [errorLabel, " Failed"] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "red", children: mergeError }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press any key to return to menu" }) })] }));
160
173
  }
161
174
  if (step === 'check-uncommitted') {
162
175
  return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: "cyan", children: "Checking for uncommitted changes..." }) }));
@@ -1,4 +1,4 @@
1
- import { ConfigScope, ProjectConfigurationData, ShortcutConfig, StatusHookConfig, WorktreeHookConfig, WorktreeConfig, CommandPresetsConfig, IConfigEditor, AutoApprovalConfig } from '../../types/index.js';
1
+ import { ConfigScope, ProjectConfigurationData, ShortcutConfig, StatusHookConfig, WorktreeHookConfig, WorktreeConfig, CommandPresetsConfig, MergeConfig, IConfigEditor, AutoApprovalConfig } from '../../types/index.js';
2
2
  /**
3
3
  * ConfigEditor provides scope-aware configuration editing.
4
4
  * The scope is determined at construction time.
@@ -24,6 +24,8 @@ export declare class ConfigEditor implements IConfigEditor {
24
24
  setWorktreeConfig(value: WorktreeConfig): void;
25
25
  getCommandPresets(): CommandPresetsConfig | undefined;
26
26
  setCommandPresets(value: CommandPresetsConfig): void;
27
+ getMergeConfig(): MergeConfig | undefined;
28
+ setMergeConfig(value: MergeConfig): void;
27
29
  getAutoApprovalConfig(): AutoApprovalConfig | undefined;
28
30
  setAutoApprovalConfig(value: AutoApprovalConfig): void;
29
31
  reload(): void;
@@ -77,6 +77,19 @@ export class ConfigEditor {
77
77
  setCommandPresets(value) {
78
78
  this.configEditor.setCommandPresets(value);
79
79
  }
80
+ getMergeConfig() {
81
+ const globalConfig = globalConfigManager.getMergeConfig();
82
+ const scopedConfig = this.configEditor.getMergeConfig();
83
+ if (!globalConfig && !scopedConfig)
84
+ return undefined;
85
+ return {
86
+ ...(globalConfig || {}),
87
+ ...(scopedConfig || {}),
88
+ };
89
+ }
90
+ setMergeConfig(value) {
91
+ this.configEditor.setMergeConfig(value);
92
+ }
80
93
  getAutoApprovalConfig() {
81
94
  const globalConfig = globalConfigManager.getAutoApprovalConfig();
82
95
  const scopedConfig = this.configEditor.getAutoApprovalConfig();
@@ -1,5 +1,5 @@
1
1
  import { Either } from 'effect';
2
- import { ShortcutConfig, StatusHookConfig, WorktreeHookConfig, WorktreeConfig, CommandPresetsConfig, CommandPreset, ConfigurationData, IConfigReader } from '../../types/index.js';
2
+ import { ShortcutConfig, StatusHookConfig, WorktreeHookConfig, WorktreeConfig, CommandPresetsConfig, MergeConfig, CommandPreset, ConfigurationData, IConfigReader } from '../../types/index.js';
3
3
  import { ValidationError } from '../../types/errors.js';
4
4
  /**
5
5
  * ConfigReader provides merged configuration reading for runtime components.
@@ -14,6 +14,7 @@ export declare class ConfigReader implements IConfigReader {
14
14
  getWorktreeHooks(): WorktreeHookConfig;
15
15
  getWorktreeConfig(): WorktreeConfig;
16
16
  getCommandPresets(): CommandPresetsConfig;
17
+ getMergeConfig(): MergeConfig | undefined;
17
18
  getConfiguration(): ConfigurationData;
18
19
  getAutoApprovalConfig(): NonNullable<ConfigurationData['autoApproval']>;
19
20
  isAutoApprovalEnabled(): boolean;
@@ -57,6 +57,17 @@ export class ConfigReader {
57
57
  ...(projectConfig || {}),
58
58
  };
59
59
  }
60
+ // Merge Config - returns merged value (project fields override global fields)
61
+ getMergeConfig() {
62
+ const globalConfig = globalConfigManager.getMergeConfig();
63
+ const projectConfig = projectConfigManager.getMergeConfig();
64
+ if (!globalConfig && !projectConfig)
65
+ return undefined;
66
+ return {
67
+ ...(globalConfig || {}),
68
+ ...(projectConfig || {}),
69
+ };
70
+ }
60
71
  // Get full merged configuration
61
72
  getConfiguration() {
62
73
  return {
@@ -65,6 +76,7 @@ export class ConfigReader {
65
76
  worktreeHooks: this.getWorktreeHooks(),
66
77
  worktree: this.getWorktreeConfig(),
67
78
  commandPresets: this.getCommandPresets(),
79
+ mergeConfig: this.getMergeConfig(),
68
80
  autoApproval: this.getAutoApprovalConfig(),
69
81
  };
70
82
  }
@@ -1,4 +1,4 @@
1
- import { ConfigurationData, StatusHookConfig, WorktreeHookConfig, ShortcutConfig, WorktreeConfig, CommandPresetsConfig, IConfigEditor } from '../../types/index.js';
1
+ import { ConfigurationData, StatusHookConfig, WorktreeHookConfig, ShortcutConfig, WorktreeConfig, CommandPresetsConfig, MergeConfig, IConfigEditor } from '../../types/index.js';
2
2
  declare class GlobalConfigManager implements IConfigEditor {
3
3
  private configPath;
4
4
  private legacyShortcutsPath;
@@ -21,6 +21,8 @@ declare class GlobalConfigManager implements IConfigEditor {
21
21
  private ensureDefaultPresets;
22
22
  getCommandPresets(): CommandPresetsConfig;
23
23
  setCommandPresets(presets: CommandPresetsConfig): void;
24
+ getMergeConfig(): MergeConfig | undefined;
25
+ setMergeConfig(mergeConfig: MergeConfig): void;
24
26
  /**
25
27
  * Reload configuration from disk
26
28
  */
@@ -191,6 +191,13 @@ class GlobalConfigManager {
191
191
  this.config.commandPresets = presets;
192
192
  this.saveConfig();
193
193
  }
194
+ getMergeConfig() {
195
+ return this.config.mergeConfig;
196
+ }
197
+ setMergeConfig(mergeConfig) {
198
+ this.config.mergeConfig = mergeConfig;
199
+ this.saveConfig();
200
+ }
194
201
  /**
195
202
  * Reload configuration from disk
196
203
  */
@@ -1,4 +1,4 @@
1
- import { ProjectConfigurationData, ShortcutConfig, StatusHookConfig, WorktreeHookConfig, WorktreeConfig, CommandPresetsConfig, IConfigEditor, AutoApprovalConfig } from '../../types/index.js';
1
+ import { ProjectConfigurationData, ShortcutConfig, StatusHookConfig, WorktreeHookConfig, WorktreeConfig, CommandPresetsConfig, MergeConfig, IConfigEditor, AutoApprovalConfig } from '../../types/index.js';
2
2
  /**
3
3
  * ProjectConfigManager handles project-specific configuration.
4
4
  * Reads/writes from `<git repository root>/.ccmanager.json`.
@@ -22,6 +22,8 @@ declare class ProjectConfigManager implements IConfigEditor {
22
22
  setWorktreeConfig(value: WorktreeConfig): void;
23
23
  getCommandPresets(): CommandPresetsConfig | undefined;
24
24
  setCommandPresets(value: CommandPresetsConfig): void;
25
+ getMergeConfig(): MergeConfig | undefined;
26
+ setMergeConfig(value: MergeConfig): void;
25
27
  getAutoApprovalConfig(): AutoApprovalConfig | undefined;
26
28
  setAutoApprovalConfig(value: AutoApprovalConfig): void;
27
29
  reload(): void;
@@ -110,6 +110,14 @@ class ProjectConfigManager {
110
110
  config.commandPresets = value;
111
111
  this.saveProjectConfig();
112
112
  }
113
+ getMergeConfig() {
114
+ return this.projectConfig?.mergeConfig;
115
+ }
116
+ setMergeConfig(value) {
117
+ const config = this.ensureProjectConfig();
118
+ config.mergeConfig = value;
119
+ this.saveProjectConfig();
120
+ }
113
121
  getAutoApprovalConfig() {
114
122
  return this.projectConfig?.autoApproval;
115
123
  }
@@ -1,5 +1,5 @@
1
1
  import { Effect } from 'effect';
2
- import { Worktree } from '../types/index.js';
2
+ import { Worktree, MergeConfig } from '../types/index.js';
3
3
  import { GitError, FileSystemError, ProcessError } from '../types/errors.js';
4
4
  /**
5
5
  * Get all worktree last opened timestamps
@@ -361,33 +361,11 @@ export declare class WorktreeService {
361
361
  *
362
362
  * @param {string} sourceBranch - Branch to merge from
363
363
  * @param {string} targetBranch - Branch to merge into
364
- * @param {boolean} useRebase - Whether to use rebase instead of merge (default: false)
364
+ * @param {'merge' | 'rebase'} operation - The merge operation to perform (default: 'merge')
365
+ * @param {MergeConfig} mergeConfig - Optional configuration for merge/rebase arguments
365
366
  * @returns {Effect.Effect<void, GitError, never>} Effect that completes successfully or fails with GitError
366
367
  *
367
- * @example
368
- * ```typescript
369
- * // Merge with Effect.all for parallel operations
370
- * await Effect.runPromise(
371
- * Effect.all([
372
- * effect1,
373
- * effect2
374
- * ], {concurrency: 1}) // Sequential to avoid conflicts
375
- * );
376
- *
377
- * // Or use Effect.catchAll for fallback behavior
378
- * const result = await Effect.runPromise(
379
- * Effect.catchAll(
380
- * effect,
381
- * (error: GitError) => {
382
- * console.error(`Merge failed: ${error.stderr}`);
383
- * // Return alternative Effect or rethrow
384
- * return Effect.fail(error);
385
- * }
386
- * )
387
- * );
388
- * ```
389
- *
390
368
  * @throws {GitError} When git merge/rebase command fails or worktrees not found
391
369
  */
392
- mergeWorktreeEffect(sourceBranch: string, targetBranch: string, useRebase?: boolean): Effect.Effect<void, GitError, never>;
370
+ mergeWorktreeEffect(sourceBranch: string, targetBranch: string, operation?: 'merge' | 'rebase', mergeConfig?: MergeConfig): Effect.Effect<void, GitError, never>;
393
371
  }
@@ -944,35 +944,13 @@ export class WorktreeService {
944
944
  *
945
945
  * @param {string} sourceBranch - Branch to merge from
946
946
  * @param {string} targetBranch - Branch to merge into
947
- * @param {boolean} useRebase - Whether to use rebase instead of merge (default: false)
947
+ * @param {'merge' | 'rebase'} operation - The merge operation to perform (default: 'merge')
948
+ * @param {MergeConfig} mergeConfig - Optional configuration for merge/rebase arguments
948
949
  * @returns {Effect.Effect<void, GitError, never>} Effect that completes successfully or fails with GitError
949
950
  *
950
- * @example
951
- * ```typescript
952
- * // Merge with Effect.all for parallel operations
953
- * await Effect.runPromise(
954
- * Effect.all([
955
- * effect1,
956
- * effect2
957
- * ], {concurrency: 1}) // Sequential to avoid conflicts
958
- * );
959
- *
960
- * // Or use Effect.catchAll for fallback behavior
961
- * const result = await Effect.runPromise(
962
- * Effect.catchAll(
963
- * effect,
964
- * (error: GitError) => {
965
- * console.error(`Merge failed: ${error.stderr}`);
966
- * // Return alternative Effect or rethrow
967
- * return Effect.fail(error);
968
- * }
969
- * )
970
- * );
971
- * ```
972
- *
973
951
  * @throws {GitError} When git merge/rebase command fails or worktrees not found
974
952
  */
975
- mergeWorktreeEffect(sourceBranch, targetBranch, useRebase = false) {
953
+ mergeWorktreeEffect(sourceBranch, targetBranch, operation = 'merge', mergeConfig) {
976
954
  // eslint-disable-next-line @typescript-eslint/no-this-alias
977
955
  const self = this;
978
956
  return Effect.gen(function* () {
@@ -981,13 +959,13 @@ export class WorktreeService {
981
959
  const targetWorktree = worktrees.find(wt => wt.branch && wt.branch.replace('refs/heads/', '') === targetBranch);
982
960
  if (!targetWorktree) {
983
961
  return yield* Effect.fail(new GitError({
984
- command: useRebase ? 'git rebase' : 'git merge',
962
+ command: operation === 'rebase' ? 'git rebase' : 'git merge',
985
963
  exitCode: 1,
986
964
  stderr: 'Target branch worktree not found',
987
965
  }));
988
966
  }
989
- // Perform the merge or rebase in the target worktree
990
- if (useRebase) {
967
+ if (operation === 'rebase') {
968
+ const rebaseArgs = mergeConfig?.rebaseArgs ?? [];
991
969
  // For rebase, we need to checkout source branch and rebase it onto target
992
970
  const sourceWorktree = worktrees.find(wt => wt.branch && wt.branch.replace('refs/heads/', '') === sourceBranch);
993
971
  if (!sourceWorktree) {
@@ -998,9 +976,12 @@ export class WorktreeService {
998
976
  }));
999
977
  }
1000
978
  // Rebase source branch onto target branch
979
+ const rebaseCmd = `git rebase ${rebaseArgs.join(' ')} "${targetBranch}"`
980
+ .replace(/\s+/g, ' ')
981
+ .trim();
1001
982
  yield* Effect.try({
1002
983
  try: () => {
1003
- execSync(`git rebase "${targetBranch}"`, {
984
+ execSync(rebaseCmd, {
1004
985
  cwd: sourceWorktree.path,
1005
986
  encoding: 'utf8',
1006
987
  });
@@ -1008,7 +989,7 @@ export class WorktreeService {
1008
989
  catch: (error) => {
1009
990
  const execError = error;
1010
991
  return new GitError({
1011
- command: `git rebase "${targetBranch}"`,
992
+ command: rebaseCmd,
1012
993
  exitCode: execError.status || 1,
1013
994
  stderr: execError.stderr || String(error),
1014
995
  stdout: execError.stdout,
@@ -1036,9 +1017,11 @@ export class WorktreeService {
1036
1017
  }
1037
1018
  else {
1038
1019
  // Regular merge
1020
+ const mergeArgs = mergeConfig?.mergeArgs ?? ['--no-ff'];
1021
+ const mergeCmd = `git merge ${mergeArgs.join(' ')} "${sourceBranch}"`;
1039
1022
  yield* Effect.try({
1040
1023
  try: () => {
1041
- execSync(`git merge --no-ff "${sourceBranch}"`, {
1024
+ execSync(mergeCmd, {
1042
1025
  cwd: targetWorktree.path,
1043
1026
  encoding: 'utf8',
1044
1027
  });
@@ -1046,7 +1029,7 @@ export class WorktreeService {
1046
1029
  catch: (error) => {
1047
1030
  const execError = error;
1048
1031
  return new GitError({
1049
- command: `git merge --no-ff "${sourceBranch}"`,
1032
+ command: mergeCmd,
1050
1033
  exitCode: execError.status || 1,
1051
1034
  stderr: execError.stderr || String(error),
1052
1035
  stdout: execError.stdout,
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,179 @@
1
+ import { describe, it, expect, afterAll, beforeEach } from 'vitest';
2
+ import { execSync } from 'child_process';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import os from 'os';
6
+ import { Effect } from 'effect';
7
+ import { WorktreeService } from './worktreeService.js';
8
+ describe('WorktreeService mergeWorktreeEffect (real git)', () => {
9
+ const testDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'ccmanager-merge-test-')));
10
+ const mainRepoDir = path.join(testDir, 'main-repo');
11
+ const sourceWorktreeDir = path.join(testDir, 'wt-source');
12
+ const targetWorktreeDir = path.join(testDir, 'wt-target');
13
+ const gitOpts = (cwd) => ({ cwd, encoding: 'utf8' });
14
+ const getCommitCount = (cwd) => {
15
+ const log = execSync('git log --oneline', gitOpts(cwd)).trim();
16
+ return log.split('\n').filter(l => l.length > 0).length;
17
+ };
18
+ const getLastCommitMessage = (cwd) => {
19
+ return execSync('git log -1 --format=%s', gitOpts(cwd)).trim();
20
+ };
21
+ const branchContainsFile = (cwd, filename) => {
22
+ return fs.existsSync(path.join(cwd, filename));
23
+ };
24
+ // Set up a fresh repo with source and target worktrees before each test
25
+ beforeEach(() => {
26
+ // Clean up previous iteration
27
+ if (fs.existsSync(mainRepoDir)) {
28
+ // Remove worktrees before deleting repo
29
+ try {
30
+ execSync(`git worktree remove "${sourceWorktreeDir}" --force`, gitOpts(mainRepoDir));
31
+ }
32
+ catch {
33
+ /* ignore */
34
+ }
35
+ try {
36
+ execSync(`git worktree remove "${targetWorktreeDir}" --force`, gitOpts(mainRepoDir));
37
+ }
38
+ catch {
39
+ /* ignore */
40
+ }
41
+ fs.rmSync(mainRepoDir, { recursive: true, force: true });
42
+ }
43
+ if (fs.existsSync(sourceWorktreeDir)) {
44
+ fs.rmSync(sourceWorktreeDir, { recursive: true, force: true });
45
+ }
46
+ if (fs.existsSync(targetWorktreeDir)) {
47
+ fs.rmSync(targetWorktreeDir, { recursive: true, force: true });
48
+ }
49
+ // Create main repository with initial commit
50
+ fs.mkdirSync(mainRepoDir, { recursive: true });
51
+ execSync('git init', gitOpts(mainRepoDir));
52
+ execSync('git config user.email "test@test.com"', gitOpts(mainRepoDir));
53
+ execSync('git config user.name "Test User"', gitOpts(mainRepoDir));
54
+ fs.writeFileSync(path.join(mainRepoDir, 'README.md'), '# Test Repo');
55
+ execSync('git add README.md', gitOpts(mainRepoDir));
56
+ execSync('git commit -m "Initial commit"', gitOpts(mainRepoDir));
57
+ // Create target branch + worktree
58
+ execSync('git branch target-branch', gitOpts(mainRepoDir));
59
+ execSync(`git worktree add "${targetWorktreeDir}" target-branch`, gitOpts(mainRepoDir));
60
+ // Create source branch + worktree
61
+ execSync('git branch source-branch', gitOpts(mainRepoDir));
62
+ execSync(`git worktree add "${sourceWorktreeDir}" source-branch`, gitOpts(mainRepoDir));
63
+ // Add commits to source branch
64
+ fs.writeFileSync(path.join(sourceWorktreeDir, 'feature-1.txt'), 'feature 1');
65
+ execSync('git add feature-1.txt', gitOpts(sourceWorktreeDir));
66
+ execSync('git commit -m "feat: add feature 1"', gitOpts(sourceWorktreeDir));
67
+ fs.writeFileSync(path.join(sourceWorktreeDir, 'feature-2.txt'), 'feature 2');
68
+ execSync('git add feature-2.txt', gitOpts(sourceWorktreeDir));
69
+ execSync('git commit -m "feat: add feature 2"', gitOpts(sourceWorktreeDir));
70
+ });
71
+ afterAll(() => {
72
+ if (fs.existsSync(testDir)) {
73
+ fs.rmSync(testDir, { recursive: true, force: true });
74
+ }
75
+ });
76
+ it('should merge with default --no-ff args', async () => {
77
+ const service = new WorktreeService(mainRepoDir);
78
+ await Effect.runPromise(service.mergeWorktreeEffect('source-branch', 'target-branch'));
79
+ // Target should now have the source files
80
+ expect(branchContainsFile(targetWorktreeDir, 'feature-1.txt')).toBe(true);
81
+ expect(branchContainsFile(targetWorktreeDir, 'feature-2.txt')).toBe(true);
82
+ // --no-ff creates a merge commit, so target should have:
83
+ // initial commit + merge commit = 4 total (initial + 2 from source + merge commit)
84
+ const count = getCommitCount(targetWorktreeDir);
85
+ expect(count).toBe(4);
86
+ // Last commit should be a merge commit
87
+ const lastMsg = getLastCommitMessage(targetWorktreeDir);
88
+ expect(lastMsg).toContain('Merge');
89
+ });
90
+ it('should merge with custom mergeArgs from MergeConfig', async () => {
91
+ const service = new WorktreeService(mainRepoDir);
92
+ await Effect.runPromise(service.mergeWorktreeEffect('source-branch', 'target-branch', 'merge', {
93
+ mergeArgs: ['--squash'],
94
+ }));
95
+ // Target should have the source files (staged by --squash)
96
+ expect(branchContainsFile(targetWorktreeDir, 'feature-1.txt')).toBe(true);
97
+ expect(branchContainsFile(targetWorktreeDir, 'feature-2.txt')).toBe(true);
98
+ // --squash does not create a merge commit automatically;
99
+ // git merge --squash stages changes but does not commit.
100
+ // The service does NOT auto-commit for plain merge, so check that
101
+ // there is no merge commit โ€” changes are just staged.
102
+ // Actually let's verify: with --squash there's no extra merge commit
103
+ // The commit count on target should still be 1 (initial) since squash only stages
104
+ const count = getCommitCount(targetWorktreeDir);
105
+ expect(count).toBe(1);
106
+ // Verify there are staged changes ready to commit
107
+ const status = execSync('git status --porcelain', gitOpts(targetWorktreeDir)).trim();
108
+ expect(status).toContain('feature-1.txt');
109
+ expect(status).toContain('feature-2.txt');
110
+ });
111
+ it('should rebase with default (empty) rebaseArgs', async () => {
112
+ const service = new WorktreeService(mainRepoDir);
113
+ await Effect.runPromise(service.mergeWorktreeEffect('source-branch', 'target-branch', 'rebase'));
114
+ // Target should now have the source files via ff-only merge after rebase
115
+ expect(branchContainsFile(targetWorktreeDir, 'feature-1.txt')).toBe(true);
116
+ expect(branchContainsFile(targetWorktreeDir, 'feature-2.txt')).toBe(true);
117
+ // Rebase + ff-only: no merge commit, linear history
118
+ // initial commit + 2 feature commits = 3
119
+ const count = getCommitCount(targetWorktreeDir);
120
+ expect(count).toBe(3);
121
+ // Commit messages should be preserved
122
+ const log = execSync('git log --oneline', gitOpts(targetWorktreeDir)).trim();
123
+ expect(log).toContain('feat: add feature 1');
124
+ expect(log).toContain('feat: add feature 2');
125
+ });
126
+ it('should rebase with custom rebaseArgs from MergeConfig', async () => {
127
+ const service = new WorktreeService(mainRepoDir);
128
+ // --no-stat is a harmless rebase flag that suppresses diffstat
129
+ await Effect.runPromise(service.mergeWorktreeEffect('source-branch', 'target-branch', 'rebase', {
130
+ rebaseArgs: ['--no-stat'],
131
+ }));
132
+ // Should still produce the same result โ€” linear history
133
+ expect(branchContainsFile(targetWorktreeDir, 'feature-1.txt')).toBe(true);
134
+ expect(branchContainsFile(targetWorktreeDir, 'feature-2.txt')).toBe(true);
135
+ const count = getCommitCount(targetWorktreeDir);
136
+ expect(count).toBe(3);
137
+ });
138
+ it('should use default args when MergeConfig is provided but mergeArgs is undefined', async () => {
139
+ const service = new WorktreeService(mainRepoDir);
140
+ // Provide MergeConfig with only rebaseArgs โ€” mergeArgs should default to --no-ff
141
+ await Effect.runPromise(service.mergeWorktreeEffect('source-branch', 'target-branch', 'merge', {
142
+ rebaseArgs: ['--no-stat'],
143
+ }));
144
+ expect(branchContainsFile(targetWorktreeDir, 'feature-1.txt')).toBe(true);
145
+ expect(branchContainsFile(targetWorktreeDir, 'feature-2.txt')).toBe(true);
146
+ // --no-ff creates a merge commit: initial + 2 source + merge = 4
147
+ const count = getCommitCount(targetWorktreeDir);
148
+ expect(count).toBe(4);
149
+ const lastMsg = getLastCommitMessage(targetWorktreeDir);
150
+ expect(lastMsg).toContain('Merge');
151
+ });
152
+ it('should fail with GitError when target worktree not found', async () => {
153
+ const service = new WorktreeService(mainRepoDir);
154
+ const result = await Effect.runPromise(Effect.either(service.mergeWorktreeEffect('source-branch', 'nonexistent-branch')));
155
+ expect(result._tag).toBe('Left');
156
+ if (result._tag === 'Left') {
157
+ expect(result.left.stderr).toContain('Target branch worktree not found');
158
+ }
159
+ });
160
+ it('should fail with GitError on merge conflict', async () => {
161
+ // Create a conflicting commit on the target branch
162
+ fs.writeFileSync(path.join(targetWorktreeDir, 'feature-1.txt'), 'conflicting content');
163
+ execSync('git add feature-1.txt', gitOpts(targetWorktreeDir));
164
+ execSync('git commit -m "conflict: add feature-1 on target"', gitOpts(targetWorktreeDir));
165
+ const service = new WorktreeService(mainRepoDir);
166
+ const result = await Effect.runPromise(Effect.either(service.mergeWorktreeEffect('source-branch', 'target-branch')));
167
+ expect(result._tag).toBe('Left');
168
+ if (result._tag === 'Left') {
169
+ expect(result.left._tag).toBe('GitError');
170
+ }
171
+ // Abort the failed merge to leave repo in clean state
172
+ try {
173
+ execSync('git merge --abort', gitOpts(targetWorktreeDir));
174
+ }
175
+ catch {
176
+ /* ignore */
177
+ }
178
+ });
179
+ });
@@ -781,7 +781,7 @@ branch refs/heads/feature
781
781
  }
782
782
  throw new Error('Command not mocked: ' + cmd);
783
783
  });
784
- const effect = service.mergeWorktreeEffect('feature', 'main', false);
784
+ const effect = service.mergeWorktreeEffect('feature', 'main', 'merge');
785
785
  await Effect.runPromise(effect);
786
786
  expect(execSync).toHaveBeenCalledWith('git merge --no-ff "feature"', expect.any(Object));
787
787
  });
@@ -800,7 +800,7 @@ branch refs/heads/main
800
800
  }
801
801
  throw new Error('Command not mocked: ' + cmd);
802
802
  });
803
- const effect = service.mergeWorktreeEffect('feature', 'nonexistent', false);
803
+ const effect = service.mergeWorktreeEffect('feature', 'nonexistent', 'merge');
804
804
  const result = await Effect.runPromise(Effect.either(effect));
805
805
  if (result._tag === 'Left') {
806
806
  expect(result.left).toBeInstanceOf(GitError);
@@ -810,6 +810,152 @@ branch refs/heads/main
810
810
  expect.fail('Should have returned Left with GitError');
811
811
  }
812
812
  });
813
+ it('should use custom merge args from MergeConfig', async () => {
814
+ mockedExecSync.mockImplementation((cmd, _options) => {
815
+ if (typeof cmd === 'string') {
816
+ if (cmd === 'git rev-parse --git-common-dir') {
817
+ return '/fake/path/.git\n';
818
+ }
819
+ if (cmd === 'git worktree list --porcelain') {
820
+ return `worktree /fake/path
821
+ HEAD abcd1234
822
+ branch refs/heads/main
823
+
824
+ worktree /fake/path/feature
825
+ HEAD efgh5678
826
+ branch refs/heads/feature
827
+ `;
828
+ }
829
+ if (cmd.includes('git merge')) {
830
+ return 'Merge successful';
831
+ }
832
+ }
833
+ throw new Error('Command not mocked: ' + cmd);
834
+ });
835
+ const effect = service.mergeWorktreeEffect('feature', 'main', 'merge', {
836
+ mergeArgs: ['--squash'],
837
+ });
838
+ await Effect.runPromise(effect);
839
+ expect(execSync).toHaveBeenCalledWith('git merge --squash "feature"', expect.any(Object));
840
+ });
841
+ it('should use default --no-ff when mergeArgs not specified in MergeConfig', async () => {
842
+ mockedExecSync.mockImplementation((cmd, _options) => {
843
+ if (typeof cmd === 'string') {
844
+ if (cmd === 'git rev-parse --git-common-dir') {
845
+ return '/fake/path/.git\n';
846
+ }
847
+ if (cmd === 'git worktree list --porcelain') {
848
+ return `worktree /fake/path
849
+ HEAD abcd1234
850
+ branch refs/heads/main
851
+
852
+ worktree /fake/path/feature
853
+ HEAD efgh5678
854
+ branch refs/heads/feature
855
+ `;
856
+ }
857
+ if (cmd.includes('git merge')) {
858
+ return 'Merge successful';
859
+ }
860
+ }
861
+ throw new Error('Command not mocked: ' + cmd);
862
+ });
863
+ const effect = service.mergeWorktreeEffect('feature', 'main', 'merge', {
864
+ rebaseArgs: ['--autosquash'],
865
+ });
866
+ await Effect.runPromise(effect);
867
+ expect(execSync).toHaveBeenCalledWith('git merge --no-ff "feature"', expect.any(Object));
868
+ });
869
+ it('should use custom rebase args from MergeConfig', async () => {
870
+ mockedExecSync.mockImplementation((cmd, _options) => {
871
+ if (typeof cmd === 'string') {
872
+ if (cmd === 'git rev-parse --git-common-dir') {
873
+ return '/fake/path/.git\n';
874
+ }
875
+ if (cmd === 'git worktree list --porcelain') {
876
+ return `worktree /fake/path
877
+ HEAD abcd1234
878
+ branch refs/heads/main
879
+
880
+ worktree /fake/path/feature
881
+ HEAD efgh5678
882
+ branch refs/heads/feature
883
+ `;
884
+ }
885
+ if (cmd.includes('git rebase')) {
886
+ return 'Rebase successful';
887
+ }
888
+ if (cmd.includes('git merge --ff-only')) {
889
+ return 'Fast-forward merge successful';
890
+ }
891
+ }
892
+ throw new Error('Command not mocked: ' + cmd);
893
+ });
894
+ const effect = service.mergeWorktreeEffect('feature', 'main', 'rebase', {
895
+ rebaseArgs: ['--autosquash', '--interactive'],
896
+ });
897
+ await Effect.runPromise(effect);
898
+ expect(execSync).toHaveBeenCalledWith('git rebase --autosquash --interactive "main"', expect.any(Object));
899
+ });
900
+ it('should use empty rebase args by default when MergeConfig has no rebaseArgs', async () => {
901
+ mockedExecSync.mockImplementation((cmd, _options) => {
902
+ if (typeof cmd === 'string') {
903
+ if (cmd === 'git rev-parse --git-common-dir') {
904
+ return '/fake/path/.git\n';
905
+ }
906
+ if (cmd === 'git worktree list --porcelain') {
907
+ return `worktree /fake/path
908
+ HEAD abcd1234
909
+ branch refs/heads/main
910
+
911
+ worktree /fake/path/feature
912
+ HEAD efgh5678
913
+ branch refs/heads/feature
914
+ `;
915
+ }
916
+ if (cmd.includes('git rebase')) {
917
+ return 'Rebase successful';
918
+ }
919
+ if (cmd.includes('git merge --ff-only')) {
920
+ return 'Fast-forward merge successful';
921
+ }
922
+ }
923
+ throw new Error('Command not mocked: ' + cmd);
924
+ });
925
+ const effect = service.mergeWorktreeEffect('feature', 'main', 'rebase', {
926
+ mergeArgs: ['--squash'],
927
+ });
928
+ await Effect.runPromise(effect);
929
+ expect(execSync).toHaveBeenCalledWith('git rebase "main"', expect.any(Object));
930
+ });
931
+ it('should use multiple merge args from MergeConfig', async () => {
932
+ mockedExecSync.mockImplementation((cmd, _options) => {
933
+ if (typeof cmd === 'string') {
934
+ if (cmd === 'git rev-parse --git-common-dir') {
935
+ return '/fake/path/.git\n';
936
+ }
937
+ if (cmd === 'git worktree list --porcelain') {
938
+ return `worktree /fake/path
939
+ HEAD abcd1234
940
+ branch refs/heads/main
941
+
942
+ worktree /fake/path/feature
943
+ HEAD efgh5678
944
+ branch refs/heads/feature
945
+ `;
946
+ }
947
+ if (cmd.includes('git merge')) {
948
+ return 'Merge successful';
949
+ }
950
+ }
951
+ throw new Error('Command not mocked: ' + cmd);
952
+ });
953
+ const effect = service.mergeWorktreeEffect('feature', 'main', 'merge', {
954
+ mergeArgs: ['--no-ff', '--no-edit'],
955
+ });
956
+ await Effect.runPromise(effect);
957
+ expect(execSync).toHaveBeenCalledWith('git merge --no-ff --no-edit "feature"', expect.any(Object));
958
+ });
813
959
  it('should return Effect that fails with GitError on merge conflict', async () => {
814
960
  mockedExecSync.mockImplementation((cmd, _options) => {
815
961
  if (typeof cmd === 'string') {
@@ -835,7 +981,7 @@ branch refs/heads/feature
835
981
  }
836
982
  throw new Error('Command not mocked: ' + cmd);
837
983
  });
838
- const effect = service.mergeWorktreeEffect('feature', 'main', false);
984
+ const effect = service.mergeWorktreeEffect('feature', 'main', 'merge');
839
985
  const result = await Effect.runPromise(Effect.either(effect));
840
986
  if (result._tag === 'Left') {
841
987
  expect(result.left).toBeInstanceOf(GitError);
@@ -86,6 +86,10 @@ export interface WorktreeConfig {
86
86
  sortByLastSession?: boolean;
87
87
  autoUseDefaultBranch?: boolean;
88
88
  }
89
+ export interface MergeConfig {
90
+ mergeArgs?: string[];
91
+ rebaseArgs?: string[];
92
+ }
89
93
  export interface CommandPreset {
90
94
  id: string;
91
95
  name: string;
@@ -109,6 +113,7 @@ export interface ConfigurationData {
109
113
  worktreeHooks?: WorktreeHookConfig;
110
114
  worktree?: WorktreeConfig;
111
115
  commandPresets?: CommandPresetsConfig;
116
+ mergeConfig?: MergeConfig;
112
117
  autoApproval?: {
113
118
  enabled: boolean;
114
119
  customCommand?: string;
@@ -129,6 +134,7 @@ export interface ProjectConfigurationData {
129
134
  worktreeHooks?: WorktreeHookConfig;
130
135
  worktree?: WorktreeConfig;
131
136
  commandPresets?: CommandPresetsConfig;
137
+ mergeConfig?: MergeConfig;
132
138
  autoApproval?: AutoApprovalConfig;
133
139
  }
134
140
  /**
@@ -142,6 +148,7 @@ export interface IConfigReader {
142
148
  getWorktreeHooks(): WorktreeHookConfig | undefined;
143
149
  getWorktreeConfig(): WorktreeConfig | undefined;
144
150
  getCommandPresets(): CommandPresetsConfig | undefined;
151
+ getMergeConfig(): MergeConfig | undefined;
145
152
  getAutoApprovalConfig(): AutoApprovalConfig | undefined;
146
153
  reload(): void;
147
154
  }
@@ -156,6 +163,7 @@ export interface IConfigEditor extends IConfigReader {
156
163
  setWorktreeHooks(value: WorktreeHookConfig): void;
157
164
  setWorktreeConfig(value: WorktreeConfig): void;
158
165
  setCommandPresets(value: CommandPresetsConfig): void;
166
+ setMergeConfig(value: MergeConfig): void;
159
167
  setAutoApprovalConfig(value: AutoApprovalConfig): void;
160
168
  }
161
169
  export interface GitProject {
@@ -211,5 +219,5 @@ export interface IWorktreeService {
211
219
  deleteWorktreeEffect(worktreePath: string, options?: {
212
220
  deleteBranch?: boolean;
213
221
  }): import('effect').Effect.Effect<void, import('../types/errors.js').GitError, never>;
214
- mergeWorktreeEffect(sourceBranch: string, targetBranch: string, useRebase?: boolean): import('effect').Effect.Effect<void, import('../types/errors.js').GitError, never>;
222
+ mergeWorktreeEffect(sourceBranch: string, targetBranch: string, operation?: 'merge' | 'rebase', mergeConfig?: MergeConfig): import('effect').Effect.Effect<void, import('../types/errors.js').GitError, never>;
215
223
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "3.8.1",
3
+ "version": "3.9.0",
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.8.1",
45
- "@kodaikabasawa/ccmanager-darwin-x64": "3.8.1",
46
- "@kodaikabasawa/ccmanager-linux-arm64": "3.8.1",
47
- "@kodaikabasawa/ccmanager-linux-x64": "3.8.1",
48
- "@kodaikabasawa/ccmanager-win32-x64": "3.8.1"
44
+ "@kodaikabasawa/ccmanager-darwin-arm64": "3.9.0",
45
+ "@kodaikabasawa/ccmanager-darwin-x64": "3.9.0",
46
+ "@kodaikabasawa/ccmanager-linux-arm64": "3.9.0",
47
+ "@kodaikabasawa/ccmanager-linux-x64": "3.9.0",
48
+ "@kodaikabasawa/ccmanager-win32-x64": "3.9.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@eslint/js": "^9.28.0",