ccmanager 3.3.2 → 3.5.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.
- package/README.md +11 -5
- package/dist/components/App.js +17 -3
- package/dist/components/App.test.js +5 -5
- package/dist/components/Configuration.d.ts +2 -0
- package/dist/components/Configuration.js +6 -2
- package/dist/components/ConfigureCommand.js +34 -11
- package/dist/components/ConfigureOther.js +18 -4
- package/dist/components/ConfigureOther.test.js +48 -12
- package/dist/components/ConfigureShortcuts.js +27 -85
- package/dist/components/ConfigureStatusHooks.js +19 -4
- package/dist/components/ConfigureStatusHooks.test.js +46 -12
- package/dist/components/ConfigureWorktree.js +18 -4
- package/dist/components/ConfigureWorktreeHooks.js +19 -4
- package/dist/components/ConfigureWorktreeHooks.test.js +49 -14
- package/dist/components/Menu.js +72 -14
- package/dist/components/Menu.recent-projects.test.js +2 -0
- package/dist/components/Menu.test.js +2 -0
- package/dist/components/NewWorktree.js +2 -2
- package/dist/components/NewWorktree.test.js +6 -6
- package/dist/components/PresetSelector.js +2 -2
- package/dist/constants/statusIcons.d.ts +4 -1
- package/dist/constants/statusIcons.js +10 -1
- package/dist/constants/statusIcons.test.js +42 -0
- package/dist/contexts/ConfigEditorContext.d.ts +21 -0
- package/dist/contexts/ConfigEditorContext.js +25 -0
- package/dist/services/autoApprovalVerifier.js +3 -3
- package/dist/services/autoApprovalVerifier.test.js +2 -2
- package/dist/services/config/configEditor.d.ts +46 -0
- package/dist/services/{configurationManager.effect.test.js → config/configEditor.effect.test.js} +46 -49
- package/dist/services/config/configEditor.js +101 -0
- package/dist/services/{configurationManager.selectPresetOnStart.test.js → config/configEditor.selectPresetOnStart.test.js} +27 -19
- package/dist/services/config/configEditor.test.d.ts +1 -0
- package/dist/services/{configurationManager.test.js → config/configEditor.test.js} +60 -132
- package/dist/services/config/configReader.d.ts +28 -0
- package/dist/services/config/configReader.js +95 -0
- package/dist/services/config/configReader.multiProject.test.d.ts +1 -0
- package/dist/services/config/configReader.multiProject.test.js +136 -0
- package/dist/services/config/globalConfigManager.d.ts +30 -0
- package/dist/services/config/globalConfigManager.js +216 -0
- package/dist/services/config/index.d.ts +13 -0
- package/dist/services/config/index.js +13 -0
- package/dist/services/config/projectConfigManager.d.ts +41 -0
- package/dist/services/config/projectConfigManager.js +181 -0
- package/dist/services/config/projectConfigManager.test.d.ts +1 -0
- package/dist/services/config/projectConfigManager.test.js +105 -0
- package/dist/services/config/testUtils.d.ts +81 -0
- package/dist/services/config/testUtils.js +351 -0
- package/dist/services/sessionManager.autoApproval.test.js +9 -6
- package/dist/services/sessionManager.d.ts +2 -0
- package/dist/services/sessionManager.effect.test.js +27 -18
- package/dist/services/sessionManager.js +43 -40
- package/dist/services/sessionManager.statePersistence.test.js +5 -4
- package/dist/services/sessionManager.test.js +71 -49
- package/dist/services/shortcutManager.d.ts +0 -1
- package/dist/services/shortcutManager.js +5 -16
- package/dist/services/shortcutManager.test.js +2 -2
- package/dist/services/stateDetector/base.d.ts +1 -0
- package/dist/services/stateDetector/claude.d.ts +1 -0
- package/dist/services/stateDetector/claude.js +8 -0
- package/dist/services/stateDetector/claude.test.js +102 -0
- package/dist/services/stateDetector/cline.d.ts +1 -0
- package/dist/services/stateDetector/cline.js +3 -0
- package/dist/services/stateDetector/codex.d.ts +1 -0
- package/dist/services/stateDetector/codex.js +3 -0
- package/dist/services/stateDetector/cursor.d.ts +1 -0
- package/dist/services/stateDetector/cursor.js +3 -0
- package/dist/services/stateDetector/gemini.d.ts +1 -0
- package/dist/services/stateDetector/gemini.js +3 -0
- package/dist/services/stateDetector/github-copilot.d.ts +1 -0
- package/dist/services/stateDetector/github-copilot.js +3 -0
- package/dist/services/stateDetector/opencode.d.ts +1 -0
- package/dist/services/stateDetector/opencode.js +3 -0
- package/dist/services/stateDetector/types.d.ts +1 -0
- package/dist/services/worktreeService.d.ts +12 -0
- package/dist/services/worktreeService.js +24 -4
- package/dist/services/worktreeService.sort.test.js +105 -109
- package/dist/services/worktreeService.test.js +5 -5
- package/dist/types/index.d.ts +47 -7
- package/dist/utils/gitUtils.d.ts +8 -0
- package/dist/utils/gitUtils.js +32 -0
- package/dist/utils/hookExecutor.js +2 -2
- package/dist/utils/hookExecutor.test.js +13 -12
- package/dist/utils/mutex.d.ts +1 -0
- package/dist/utils/mutex.js +1 -0
- package/dist/utils/worktreeUtils.js +3 -2
- package/dist/utils/worktreeUtils.test.js +2 -1
- package/package.json +7 -7
- package/dist/services/configurationManager.d.ts +0 -121
- package/dist/services/configurationManager.js +0 -597
- /package/dist/{services/configurationManager.effect.test.d.ts → constants/statusIcons.test.d.ts} +0 -0
- /package/dist/services/{configurationManager.selectPresetOnStart.test.d.ts → config/configEditor.effect.test.d.ts} +0 -0
- /package/dist/services/{configurationManager.test.d.ts → config/configEditor.selectPresetOnStart.test.d.ts} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
-
import {
|
|
3
|
+
import { ConfigEditor } from './configEditor.js';
|
|
4
|
+
import { addPreset, deletePreset, setDefaultPreset, getDefaultPreset, } from './testUtils.js';
|
|
4
5
|
// Mock fs module
|
|
5
6
|
vi.mock('fs', () => ({
|
|
6
7
|
existsSync: vi.fn(),
|
|
@@ -12,44 +13,50 @@ vi.mock('fs', () => ({
|
|
|
12
13
|
vi.mock('os', () => ({
|
|
13
14
|
homedir: vi.fn(() => '/home/test'),
|
|
14
15
|
}));
|
|
15
|
-
describe('
|
|
16
|
-
let
|
|
16
|
+
describe('ConfigEditor (global scope) - Command Presets', () => {
|
|
17
|
+
let configEditor;
|
|
17
18
|
let mockConfigData;
|
|
19
|
+
let savedConfigData = null;
|
|
20
|
+
// Helper to reset saved config when modifying mockConfigData
|
|
21
|
+
const resetSavedConfig = () => {
|
|
22
|
+
savedConfigData = null;
|
|
23
|
+
};
|
|
18
24
|
beforeEach(() => {
|
|
19
25
|
// Reset all mocks
|
|
20
26
|
vi.clearAllMocks();
|
|
27
|
+
savedConfigData = null;
|
|
21
28
|
// Default mock config data
|
|
22
29
|
mockConfigData = {
|
|
23
30
|
shortcuts: {
|
|
24
31
|
returnToMenu: { ctrl: true, key: 'e' },
|
|
25
32
|
cancel: { key: 'escape' },
|
|
26
33
|
},
|
|
27
|
-
command: {
|
|
28
|
-
command: 'claude',
|
|
29
|
-
args: ['--existing'],
|
|
30
|
-
},
|
|
31
34
|
};
|
|
32
35
|
// Mock file system operations
|
|
33
36
|
existsSync.mockImplementation((path) => {
|
|
34
37
|
return path.includes('config.json');
|
|
35
38
|
});
|
|
36
39
|
readFileSync.mockImplementation(() => {
|
|
37
|
-
return
|
|
40
|
+
// Return saved data if available, otherwise return initial mock data
|
|
41
|
+
return savedConfigData ?? JSON.stringify(mockConfigData);
|
|
38
42
|
});
|
|
39
43
|
mkdirSync.mockImplementation(() => { });
|
|
40
|
-
writeFileSync.mockImplementation(() => {
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
writeFileSync.mockImplementation((_path, data) => {
|
|
45
|
+
// Track written data so subsequent reads return it
|
|
46
|
+
savedConfigData = data;
|
|
47
|
+
});
|
|
48
|
+
// Create new instance for each test and reload to pick up mocked fs
|
|
49
|
+
configEditor = new ConfigEditor('global');
|
|
50
|
+
configEditor.reload();
|
|
43
51
|
});
|
|
44
52
|
afterEach(() => {
|
|
45
53
|
vi.resetAllMocks();
|
|
46
54
|
});
|
|
47
55
|
describe('getCommandPresets', () => {
|
|
48
56
|
it('should return default presets when no presets are configured', () => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const presets = configManager.getCommandPresets();
|
|
57
|
+
resetSavedConfig();
|
|
58
|
+
configEditor.reload();
|
|
59
|
+
const presets = configEditor.getCommandPresets();
|
|
53
60
|
expect(presets).toBeDefined();
|
|
54
61
|
expect(presets.presets).toHaveLength(1);
|
|
55
62
|
expect(presets.presets[0]).toEqual({
|
|
@@ -67,33 +74,12 @@ describe('ConfigurationManager - Command Presets', () => {
|
|
|
67
74
|
],
|
|
68
75
|
defaultPresetId: '2',
|
|
69
76
|
};
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
resetSavedConfig();
|
|
78
|
+
configEditor.reload();
|
|
79
|
+
const presets = configEditor.getCommandPresets();
|
|
72
80
|
expect(presets.presets).toHaveLength(2);
|
|
73
81
|
expect(presets.defaultPresetId).toBe('2');
|
|
74
82
|
});
|
|
75
|
-
it('should migrate legacy command config to presets on first access', () => {
|
|
76
|
-
// Config has legacy command but no presets
|
|
77
|
-
mockConfigData.command = {
|
|
78
|
-
command: 'claude',
|
|
79
|
-
args: ['--resume'],
|
|
80
|
-
fallbackArgs: ['--no-mcp'],
|
|
81
|
-
};
|
|
82
|
-
delete mockConfigData.commandPresets;
|
|
83
|
-
configManager = new ConfigurationManager();
|
|
84
|
-
const presets = configManager.getCommandPresets();
|
|
85
|
-
expect(presets.presets).toHaveLength(1);
|
|
86
|
-
expect(presets.presets[0]).toEqual({
|
|
87
|
-
id: '1',
|
|
88
|
-
name: 'Main',
|
|
89
|
-
command: 'claude',
|
|
90
|
-
args: ['--resume'],
|
|
91
|
-
fallbackArgs: ['--no-mcp'],
|
|
92
|
-
});
|
|
93
|
-
expect(presets.defaultPresetId).toBe('1');
|
|
94
|
-
// Verify that writeFileSync was called to save the migration
|
|
95
|
-
expect(writeFileSync).toHaveBeenCalled();
|
|
96
|
-
});
|
|
97
83
|
});
|
|
98
84
|
describe('setCommandPresets', () => {
|
|
99
85
|
it('should save new presets configuration', () => {
|
|
@@ -104,7 +90,7 @@ describe('ConfigurationManager - Command Presets', () => {
|
|
|
104
90
|
],
|
|
105
91
|
defaultPresetId: '2',
|
|
106
92
|
};
|
|
107
|
-
|
|
93
|
+
configEditor.setCommandPresets(newPresets);
|
|
108
94
|
expect(writeFileSync).toHaveBeenCalledWith(expect.stringContaining('config.json'), expect.stringContaining('commandPresets'));
|
|
109
95
|
});
|
|
110
96
|
});
|
|
@@ -117,8 +103,9 @@ describe('ConfigurationManager - Command Presets', () => {
|
|
|
117
103
|
],
|
|
118
104
|
defaultPresetId: '2',
|
|
119
105
|
};
|
|
120
|
-
|
|
121
|
-
|
|
106
|
+
resetSavedConfig();
|
|
107
|
+
configEditor.reload();
|
|
108
|
+
const defaultPreset = getDefaultPreset(configEditor);
|
|
122
109
|
expect(defaultPreset).toEqual({
|
|
123
110
|
id: '2',
|
|
124
111
|
name: 'Custom',
|
|
@@ -134,8 +121,9 @@ describe('ConfigurationManager - Command Presets', () => {
|
|
|
134
121
|
],
|
|
135
122
|
defaultPresetId: 'invalid',
|
|
136
123
|
};
|
|
137
|
-
|
|
138
|
-
|
|
124
|
+
resetSavedConfig();
|
|
125
|
+
configEditor.reload();
|
|
126
|
+
const defaultPreset = getDefaultPreset(configEditor);
|
|
139
127
|
expect(defaultPreset).toEqual({
|
|
140
128
|
id: '1',
|
|
141
129
|
name: 'Main',
|
|
@@ -143,49 +131,22 @@ describe('ConfigurationManager - Command Presets', () => {
|
|
|
143
131
|
});
|
|
144
132
|
});
|
|
145
133
|
});
|
|
146
|
-
describe('getPresetById', () => {
|
|
147
|
-
it('should return preset by id', () => {
|
|
148
|
-
mockConfigData.commandPresets = {
|
|
149
|
-
presets: [
|
|
150
|
-
{ id: '1', name: 'Main', command: 'claude' },
|
|
151
|
-
{ id: '2', name: 'Custom', command: 'claude', args: ['--custom'] },
|
|
152
|
-
],
|
|
153
|
-
defaultPresetId: '1',
|
|
154
|
-
};
|
|
155
|
-
configManager = new ConfigurationManager();
|
|
156
|
-
const preset = configManager.getPresetById('2');
|
|
157
|
-
expect(preset).toEqual({
|
|
158
|
-
id: '2',
|
|
159
|
-
name: 'Custom',
|
|
160
|
-
command: 'claude',
|
|
161
|
-
args: ['--custom'],
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
it('should return undefined for non-existent id', () => {
|
|
165
|
-
mockConfigData.commandPresets = {
|
|
166
|
-
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
167
|
-
defaultPresetId: '1',
|
|
168
|
-
};
|
|
169
|
-
configManager = new ConfigurationManager();
|
|
170
|
-
const preset = configManager.getPresetById('999');
|
|
171
|
-
expect(preset).toBeUndefined();
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
134
|
describe('addPreset', () => {
|
|
175
135
|
it('should add a new preset', () => {
|
|
176
136
|
mockConfigData.commandPresets = {
|
|
177
137
|
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
178
138
|
defaultPresetId: '1',
|
|
179
139
|
};
|
|
180
|
-
|
|
140
|
+
resetSavedConfig();
|
|
141
|
+
configEditor.reload();
|
|
181
142
|
const newPreset = {
|
|
182
143
|
id: '2',
|
|
183
144
|
name: 'New Preset',
|
|
184
145
|
command: 'claude',
|
|
185
146
|
args: ['--new'],
|
|
186
147
|
};
|
|
187
|
-
|
|
188
|
-
const presets =
|
|
148
|
+
addPreset(configEditor, newPreset);
|
|
149
|
+
const presets = configEditor.getCommandPresets();
|
|
189
150
|
expect(presets.presets).toHaveLength(2);
|
|
190
151
|
expect(presets.presets[1]).toEqual(newPreset);
|
|
191
152
|
});
|
|
@@ -194,15 +155,16 @@ describe('ConfigurationManager - Command Presets', () => {
|
|
|
194
155
|
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
195
156
|
defaultPresetId: '1',
|
|
196
157
|
};
|
|
197
|
-
|
|
158
|
+
resetSavedConfig();
|
|
159
|
+
configEditor.reload();
|
|
198
160
|
const updatedPreset = {
|
|
199
161
|
id: '1',
|
|
200
162
|
name: 'Updated Default',
|
|
201
163
|
command: 'claude',
|
|
202
164
|
args: ['--updated'],
|
|
203
165
|
};
|
|
204
|
-
|
|
205
|
-
const presets =
|
|
166
|
+
addPreset(configEditor, updatedPreset);
|
|
167
|
+
const presets = configEditor.getCommandPresets();
|
|
206
168
|
expect(presets.presets).toHaveLength(1);
|
|
207
169
|
expect(presets.presets[0]).toEqual(updatedPreset);
|
|
208
170
|
});
|
|
@@ -216,9 +178,10 @@ describe('ConfigurationManager - Command Presets', () => {
|
|
|
216
178
|
],
|
|
217
179
|
defaultPresetId: '1',
|
|
218
180
|
};
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
181
|
+
resetSavedConfig();
|
|
182
|
+
configEditor.reload();
|
|
183
|
+
deletePreset(configEditor, '2');
|
|
184
|
+
const presets = configEditor.getCommandPresets();
|
|
222
185
|
expect(presets.presets).toHaveLength(1);
|
|
223
186
|
expect(presets.presets[0].id).toBe('1');
|
|
224
187
|
});
|
|
@@ -227,9 +190,10 @@ describe('ConfigurationManager - Command Presets', () => {
|
|
|
227
190
|
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
228
191
|
defaultPresetId: '1',
|
|
229
192
|
};
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
193
|
+
resetSavedConfig();
|
|
194
|
+
configEditor.reload();
|
|
195
|
+
deletePreset(configEditor, '1');
|
|
196
|
+
const presets = configEditor.getCommandPresets();
|
|
233
197
|
expect(presets.presets).toHaveLength(1);
|
|
234
198
|
});
|
|
235
199
|
it('should update defaultPresetId if default preset is deleted', () => {
|
|
@@ -240,9 +204,10 @@ describe('ConfigurationManager - Command Presets', () => {
|
|
|
240
204
|
],
|
|
241
205
|
defaultPresetId: '2',
|
|
242
206
|
};
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
207
|
+
resetSavedConfig();
|
|
208
|
+
configEditor.reload();
|
|
209
|
+
deletePreset(configEditor, '2');
|
|
210
|
+
const presets = configEditor.getCommandPresets();
|
|
246
211
|
expect(presets.defaultPresetId).toBe('1');
|
|
247
212
|
});
|
|
248
213
|
});
|
|
@@ -255,9 +220,10 @@ describe('ConfigurationManager - Command Presets', () => {
|
|
|
255
220
|
],
|
|
256
221
|
defaultPresetId: '1',
|
|
257
222
|
};
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
223
|
+
resetSavedConfig();
|
|
224
|
+
configEditor.reload();
|
|
225
|
+
setDefaultPreset(configEditor, '2');
|
|
226
|
+
const presets = configEditor.getCommandPresets();
|
|
261
227
|
expect(presets.defaultPresetId).toBe('2');
|
|
262
228
|
});
|
|
263
229
|
it('should not update if preset id does not exist', () => {
|
|
@@ -265,49 +231,11 @@ describe('ConfigurationManager - Command Presets', () => {
|
|
|
265
231
|
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
266
232
|
defaultPresetId: '1',
|
|
267
233
|
};
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
234
|
+
resetSavedConfig();
|
|
235
|
+
configEditor.reload();
|
|
236
|
+
setDefaultPreset(configEditor, '999');
|
|
237
|
+
const presets = configEditor.getCommandPresets();
|
|
271
238
|
expect(presets.defaultPresetId).toBe('1');
|
|
272
239
|
});
|
|
273
240
|
});
|
|
274
|
-
describe('backward compatibility', () => {
|
|
275
|
-
it('should maintain getCommandConfig for backward compatibility', () => {
|
|
276
|
-
mockConfigData.commandPresets = {
|
|
277
|
-
presets: [
|
|
278
|
-
{ id: '1', name: 'Main', command: 'claude', args: ['--resume'] },
|
|
279
|
-
{ id: '2', name: 'Custom', command: 'claude', args: ['--custom'] },
|
|
280
|
-
],
|
|
281
|
-
defaultPresetId: '1',
|
|
282
|
-
};
|
|
283
|
-
configManager = new ConfigurationManager();
|
|
284
|
-
const commandConfig = configManager.getCommandConfig();
|
|
285
|
-
// Should return the default preset as CommandConfig
|
|
286
|
-
expect(commandConfig).toEqual({
|
|
287
|
-
command: 'claude',
|
|
288
|
-
args: ['--resume'],
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
it('should update default preset when setCommandConfig is called', () => {
|
|
292
|
-
mockConfigData.commandPresets = {
|
|
293
|
-
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
294
|
-
defaultPresetId: '1',
|
|
295
|
-
};
|
|
296
|
-
configManager = new ConfigurationManager();
|
|
297
|
-
const newConfig = {
|
|
298
|
-
command: 'claude',
|
|
299
|
-
args: ['--new-args'],
|
|
300
|
-
fallbackArgs: ['--new-fallback'],
|
|
301
|
-
};
|
|
302
|
-
configManager.setCommandConfig(newConfig);
|
|
303
|
-
const presets = configManager.getCommandPresets();
|
|
304
|
-
expect(presets.presets[0]).toEqual({
|
|
305
|
-
id: '1',
|
|
306
|
-
name: 'Main',
|
|
307
|
-
command: 'claude',
|
|
308
|
-
args: ['--new-args'],
|
|
309
|
-
fallbackArgs: ['--new-fallback'],
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
241
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Either } from 'effect';
|
|
2
|
+
import { ShortcutConfig, StatusHookConfig, WorktreeHookConfig, WorktreeConfig, CommandPresetsConfig, CommandPreset, ConfigurationData, IConfigReader } from '../../types/index.js';
|
|
3
|
+
import { ValidationError } from '../../types/errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* ConfigReader provides merged configuration reading for runtime components.
|
|
6
|
+
* It combines project-level config (from `.ccmanager.json`) with global config,
|
|
7
|
+
* with project config taking priority.
|
|
8
|
+
*
|
|
9
|
+
* Uses the singleton projectConfigManager (cwd-based) for project config.
|
|
10
|
+
*/
|
|
11
|
+
export declare class ConfigReader implements IConfigReader {
|
|
12
|
+
getShortcuts(): ShortcutConfig;
|
|
13
|
+
getStatusHooks(): StatusHookConfig;
|
|
14
|
+
getWorktreeHooks(): WorktreeHookConfig;
|
|
15
|
+
getWorktreeConfig(): WorktreeConfig;
|
|
16
|
+
getCommandPresets(): CommandPresetsConfig;
|
|
17
|
+
getConfiguration(): ConfigurationData;
|
|
18
|
+
getAutoApprovalConfig(): NonNullable<ConfigurationData['autoApproval']>;
|
|
19
|
+
isAutoApprovalEnabled(): boolean;
|
|
20
|
+
getDefaultPreset(): CommandPreset;
|
|
21
|
+
getSelectPresetOnStart(): boolean;
|
|
22
|
+
getPresetByIdEffect(id: string): Either.Either<CommandPreset, ValidationError>;
|
|
23
|
+
reload(): void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Default singleton instance
|
|
27
|
+
*/
|
|
28
|
+
export declare const configReader: ConfigReader;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Either } from 'effect';
|
|
2
|
+
import { ValidationError } from '../../types/errors.js';
|
|
3
|
+
import { globalConfigManager } from './globalConfigManager.js';
|
|
4
|
+
import { projectConfigManager } from './projectConfigManager.js';
|
|
5
|
+
/**
|
|
6
|
+
* ConfigReader provides merged configuration reading for runtime components.
|
|
7
|
+
* It combines project-level config (from `.ccmanager.json`) with global config,
|
|
8
|
+
* with project config taking priority.
|
|
9
|
+
*
|
|
10
|
+
* Uses the singleton projectConfigManager (cwd-based) for project config.
|
|
11
|
+
*/
|
|
12
|
+
export class ConfigReader {
|
|
13
|
+
// Shortcuts - returns merged value (project > global)
|
|
14
|
+
getShortcuts() {
|
|
15
|
+
return (projectConfigManager.getShortcuts() || globalConfigManager.getShortcuts());
|
|
16
|
+
}
|
|
17
|
+
// Status Hooks - returns merged value (project > global)
|
|
18
|
+
getStatusHooks() {
|
|
19
|
+
return (projectConfigManager.getStatusHooks() ||
|
|
20
|
+
globalConfigManager.getStatusHooks());
|
|
21
|
+
}
|
|
22
|
+
// Worktree Hooks - returns merged value (project > global)
|
|
23
|
+
getWorktreeHooks() {
|
|
24
|
+
return (projectConfigManager.getWorktreeHooks() ||
|
|
25
|
+
globalConfigManager.getWorktreeHooks());
|
|
26
|
+
}
|
|
27
|
+
// Worktree Config - returns merged value (project > global)
|
|
28
|
+
getWorktreeConfig() {
|
|
29
|
+
return (projectConfigManager.getWorktreeConfig() ||
|
|
30
|
+
globalConfigManager.getWorktreeConfig());
|
|
31
|
+
}
|
|
32
|
+
// Command Presets - returns merged value (project > global)
|
|
33
|
+
getCommandPresets() {
|
|
34
|
+
return (projectConfigManager.getCommandPresets() ||
|
|
35
|
+
globalConfigManager.getCommandPresets());
|
|
36
|
+
}
|
|
37
|
+
// Get full merged configuration
|
|
38
|
+
getConfiguration() {
|
|
39
|
+
return {
|
|
40
|
+
shortcuts: this.getShortcuts(),
|
|
41
|
+
statusHooks: this.getStatusHooks(),
|
|
42
|
+
worktreeHooks: this.getWorktreeHooks(),
|
|
43
|
+
worktree: this.getWorktreeConfig(),
|
|
44
|
+
commandPresets: this.getCommandPresets(),
|
|
45
|
+
autoApproval: this.getAutoApprovalConfig(),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// Auto Approval Config - returns merged value (project > global)
|
|
49
|
+
getAutoApprovalConfig() {
|
|
50
|
+
const projectConfig = projectConfigManager.getAutoApprovalConfig();
|
|
51
|
+
if (projectConfig) {
|
|
52
|
+
return {
|
|
53
|
+
...projectConfig,
|
|
54
|
+
timeout: projectConfig.timeout ?? 30,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return globalConfigManager.getAutoApprovalConfig();
|
|
58
|
+
}
|
|
59
|
+
// Check if auto-approval is enabled
|
|
60
|
+
isAutoApprovalEnabled() {
|
|
61
|
+
return this.getAutoApprovalConfig().enabled;
|
|
62
|
+
}
|
|
63
|
+
// Command Preset methods - delegate to global config for modifications
|
|
64
|
+
getDefaultPreset() {
|
|
65
|
+
const presets = this.getCommandPresets();
|
|
66
|
+
const defaultPreset = presets.presets.find(p => p.id === presets.defaultPresetId);
|
|
67
|
+
return defaultPreset || presets.presets[0];
|
|
68
|
+
}
|
|
69
|
+
getSelectPresetOnStart() {
|
|
70
|
+
const presets = this.getCommandPresets();
|
|
71
|
+
return presets.selectPresetOnStart ?? false;
|
|
72
|
+
}
|
|
73
|
+
// Get preset by ID with Either-based error handling
|
|
74
|
+
getPresetByIdEffect(id) {
|
|
75
|
+
const presets = this.getCommandPresets();
|
|
76
|
+
const preset = presets.presets.find(p => p.id === id);
|
|
77
|
+
if (!preset) {
|
|
78
|
+
return Either.left(new ValidationError({
|
|
79
|
+
field: 'presetId',
|
|
80
|
+
constraint: 'Preset not found',
|
|
81
|
+
receivedValue: id,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
return Either.right(preset);
|
|
85
|
+
}
|
|
86
|
+
// Reload both project and global configs from disk
|
|
87
|
+
reload() {
|
|
88
|
+
projectConfigManager.reload();
|
|
89
|
+
globalConfigManager.reload();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Default singleton instance
|
|
94
|
+
*/
|
|
95
|
+
export const configReader = new ConfigReader();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
|
+
import { ENV_VARS } from '../../constants/env.js';
|
|
4
|
+
// Mock fs module
|
|
5
|
+
vi.mock('fs', () => ({
|
|
6
|
+
existsSync: vi.fn(),
|
|
7
|
+
mkdirSync: vi.fn(),
|
|
8
|
+
readFileSync: vi.fn(),
|
|
9
|
+
writeFileSync: vi.fn(),
|
|
10
|
+
unlinkSync: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
// Mock os module
|
|
13
|
+
vi.mock('os', () => ({
|
|
14
|
+
homedir: vi.fn(() => '/home/test'),
|
|
15
|
+
}));
|
|
16
|
+
describe('ConfigReader in multi-project mode', () => {
|
|
17
|
+
let originalEnv;
|
|
18
|
+
// Global config data
|
|
19
|
+
const globalConfigData = {
|
|
20
|
+
shortcuts: {
|
|
21
|
+
returnToMenu: { ctrl: true, key: 'g' },
|
|
22
|
+
cancel: { key: 'escape' },
|
|
23
|
+
},
|
|
24
|
+
commandPresets: {
|
|
25
|
+
presets: [{ id: 'global-1', name: 'Global Preset', command: 'claude' }],
|
|
26
|
+
defaultPresetId: 'global-1',
|
|
27
|
+
},
|
|
28
|
+
worktree: {
|
|
29
|
+
autoDirectory: false,
|
|
30
|
+
copySessionData: true,
|
|
31
|
+
sortByLastSession: false,
|
|
32
|
+
},
|
|
33
|
+
autoApproval: {
|
|
34
|
+
enabled: false,
|
|
35
|
+
timeout: 30,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
// Project config data (should be ignored in multi-project mode)
|
|
39
|
+
const projectConfigData = {
|
|
40
|
+
shortcuts: {
|
|
41
|
+
returnToMenu: { ctrl: true, key: 'p' },
|
|
42
|
+
cancel: { key: 'q' },
|
|
43
|
+
},
|
|
44
|
+
commandPresets: {
|
|
45
|
+
presets: [
|
|
46
|
+
{ id: 'project-1', name: 'Project Preset', command: 'claude-project' },
|
|
47
|
+
],
|
|
48
|
+
defaultPresetId: 'project-1',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
vi.clearAllMocks();
|
|
53
|
+
vi.resetModules();
|
|
54
|
+
// Save original env
|
|
55
|
+
originalEnv = process.env[ENV_VARS.MULTI_PROJECT_ROOT];
|
|
56
|
+
// Mock file system
|
|
57
|
+
existsSync.mockImplementation((path) => {
|
|
58
|
+
if (path.includes('.ccmanager.json')) {
|
|
59
|
+
return true; // Project config exists
|
|
60
|
+
}
|
|
61
|
+
if (path.includes('config.json')) {
|
|
62
|
+
return true; // Global config exists
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
});
|
|
66
|
+
readFileSync.mockImplementation((path) => {
|
|
67
|
+
if (path.includes('.ccmanager.json')) {
|
|
68
|
+
return JSON.stringify(projectConfigData);
|
|
69
|
+
}
|
|
70
|
+
if (path.includes('config.json')) {
|
|
71
|
+
return JSON.stringify(globalConfigData);
|
|
72
|
+
}
|
|
73
|
+
return '{}';
|
|
74
|
+
});
|
|
75
|
+
mkdirSync.mockImplementation(() => { });
|
|
76
|
+
writeFileSync.mockImplementation(() => { });
|
|
77
|
+
});
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
// Restore original env
|
|
80
|
+
if (originalEnv !== undefined) {
|
|
81
|
+
process.env[ENV_VARS.MULTI_PROJECT_ROOT] = originalEnv;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
delete process.env[ENV_VARS.MULTI_PROJECT_ROOT];
|
|
85
|
+
}
|
|
86
|
+
vi.resetAllMocks();
|
|
87
|
+
});
|
|
88
|
+
it('should return global config when CCMANAGER_MULTI_PROJECT_ROOT is set', async () => {
|
|
89
|
+
// Set multi-project mode
|
|
90
|
+
process.env[ENV_VARS.MULTI_PROJECT_ROOT] = '/path/to/projects';
|
|
91
|
+
// Dynamic import to pick up the env var
|
|
92
|
+
const { ConfigReader } = await import('./configReader.js');
|
|
93
|
+
const reader = new ConfigReader();
|
|
94
|
+
reader.reload();
|
|
95
|
+
// Verify global config is returned (not project config)
|
|
96
|
+
const shortcuts = reader.getShortcuts();
|
|
97
|
+
expect(shortcuts.returnToMenu).toEqual({ ctrl: true, key: 'g' });
|
|
98
|
+
const presets = reader.getCommandPresets();
|
|
99
|
+
expect(presets.presets[0].id).toBe('global-1');
|
|
100
|
+
expect(presets.presets[0].name).toBe('Global Preset');
|
|
101
|
+
});
|
|
102
|
+
it('should return project config when CCMANAGER_MULTI_PROJECT_ROOT is not set', async () => {
|
|
103
|
+
// Ensure multi-project mode is NOT set
|
|
104
|
+
delete process.env[ENV_VARS.MULTI_PROJECT_ROOT];
|
|
105
|
+
// Dynamic import
|
|
106
|
+
const { ConfigReader } = await import('./configReader.js');
|
|
107
|
+
const reader = new ConfigReader();
|
|
108
|
+
reader.reload();
|
|
109
|
+
// Verify project config takes priority
|
|
110
|
+
const shortcuts = reader.getShortcuts();
|
|
111
|
+
expect(shortcuts.returnToMenu).toEqual({ ctrl: true, key: 'p' });
|
|
112
|
+
const presets = reader.getCommandPresets();
|
|
113
|
+
expect(presets.presets[0].id).toBe('project-1');
|
|
114
|
+
expect(presets.presets[0].name).toBe('Project Preset');
|
|
115
|
+
});
|
|
116
|
+
it('should return global autoApproval config in multi-project mode', async () => {
|
|
117
|
+
// Set multi-project mode
|
|
118
|
+
process.env[ENV_VARS.MULTI_PROJECT_ROOT] = '/path/to/projects';
|
|
119
|
+
const { ConfigReader } = await import('./configReader.js');
|
|
120
|
+
const reader = new ConfigReader();
|
|
121
|
+
reader.reload();
|
|
122
|
+
const autoApproval = reader.getAutoApprovalConfig();
|
|
123
|
+
expect(autoApproval.enabled).toBe(false);
|
|
124
|
+
expect(autoApproval.timeout).toBe(30);
|
|
125
|
+
});
|
|
126
|
+
it('should return global worktree config in multi-project mode', async () => {
|
|
127
|
+
// Set multi-project mode
|
|
128
|
+
process.env[ENV_VARS.MULTI_PROJECT_ROOT] = '/path/to/projects';
|
|
129
|
+
const { ConfigReader } = await import('./configReader.js');
|
|
130
|
+
const reader = new ConfigReader();
|
|
131
|
+
reader.reload();
|
|
132
|
+
const worktreeConfig = reader.getWorktreeConfig();
|
|
133
|
+
expect(worktreeConfig.autoDirectory).toBe(false);
|
|
134
|
+
expect(worktreeConfig.copySessionData).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ConfigurationData, StatusHookConfig, WorktreeHookConfig, ShortcutConfig, WorktreeConfig, CommandPresetsConfig, IConfigEditor } from '../../types/index.js';
|
|
2
|
+
declare class GlobalConfigManager implements IConfigEditor {
|
|
3
|
+
private configPath;
|
|
4
|
+
private legacyShortcutsPath;
|
|
5
|
+
private configDir;
|
|
6
|
+
private config;
|
|
7
|
+
constructor();
|
|
8
|
+
private loadConfig;
|
|
9
|
+
private migrateLegacyShortcuts;
|
|
10
|
+
private saveConfig;
|
|
11
|
+
getShortcuts(): ShortcutConfig;
|
|
12
|
+
setShortcuts(shortcuts: ShortcutConfig): void;
|
|
13
|
+
getStatusHooks(): StatusHookConfig;
|
|
14
|
+
setStatusHooks(hooks: StatusHookConfig): void;
|
|
15
|
+
getWorktreeHooks(): WorktreeHookConfig;
|
|
16
|
+
setWorktreeHooks(hooks: WorktreeHookConfig): void;
|
|
17
|
+
getWorktreeConfig(): WorktreeConfig;
|
|
18
|
+
setWorktreeConfig(worktreeConfig: WorktreeConfig): void;
|
|
19
|
+
getAutoApprovalConfig(): NonNullable<ConfigurationData['autoApproval']>;
|
|
20
|
+
setAutoApprovalConfig(autoApproval: NonNullable<ConfigurationData['autoApproval']>): void;
|
|
21
|
+
private ensureDefaultPresets;
|
|
22
|
+
getCommandPresets(): CommandPresetsConfig;
|
|
23
|
+
setCommandPresets(presets: CommandPresetsConfig): void;
|
|
24
|
+
/**
|
|
25
|
+
* Reload configuration from disk
|
|
26
|
+
*/
|
|
27
|
+
reload(): void;
|
|
28
|
+
}
|
|
29
|
+
export declare const globalConfigManager: GlobalConfigManager;
|
|
30
|
+
export {};
|