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.
Files changed (92) hide show
  1. package/README.md +11 -5
  2. package/dist/components/App.js +17 -3
  3. package/dist/components/App.test.js +5 -5
  4. package/dist/components/Configuration.d.ts +2 -0
  5. package/dist/components/Configuration.js +6 -2
  6. package/dist/components/ConfigureCommand.js +34 -11
  7. package/dist/components/ConfigureOther.js +18 -4
  8. package/dist/components/ConfigureOther.test.js +48 -12
  9. package/dist/components/ConfigureShortcuts.js +27 -85
  10. package/dist/components/ConfigureStatusHooks.js +19 -4
  11. package/dist/components/ConfigureStatusHooks.test.js +46 -12
  12. package/dist/components/ConfigureWorktree.js +18 -4
  13. package/dist/components/ConfigureWorktreeHooks.js +19 -4
  14. package/dist/components/ConfigureWorktreeHooks.test.js +49 -14
  15. package/dist/components/Menu.js +72 -14
  16. package/dist/components/Menu.recent-projects.test.js +2 -0
  17. package/dist/components/Menu.test.js +2 -0
  18. package/dist/components/NewWorktree.js +2 -2
  19. package/dist/components/NewWorktree.test.js +6 -6
  20. package/dist/components/PresetSelector.js +2 -2
  21. package/dist/constants/statusIcons.d.ts +4 -1
  22. package/dist/constants/statusIcons.js +10 -1
  23. package/dist/constants/statusIcons.test.js +42 -0
  24. package/dist/contexts/ConfigEditorContext.d.ts +21 -0
  25. package/dist/contexts/ConfigEditorContext.js +25 -0
  26. package/dist/services/autoApprovalVerifier.js +3 -3
  27. package/dist/services/autoApprovalVerifier.test.js +2 -2
  28. package/dist/services/config/configEditor.d.ts +46 -0
  29. package/dist/services/{configurationManager.effect.test.js → config/configEditor.effect.test.js} +46 -49
  30. package/dist/services/config/configEditor.js +101 -0
  31. package/dist/services/{configurationManager.selectPresetOnStart.test.js → config/configEditor.selectPresetOnStart.test.js} +27 -19
  32. package/dist/services/config/configEditor.test.d.ts +1 -0
  33. package/dist/services/{configurationManager.test.js → config/configEditor.test.js} +60 -132
  34. package/dist/services/config/configReader.d.ts +28 -0
  35. package/dist/services/config/configReader.js +95 -0
  36. package/dist/services/config/configReader.multiProject.test.d.ts +1 -0
  37. package/dist/services/config/configReader.multiProject.test.js +136 -0
  38. package/dist/services/config/globalConfigManager.d.ts +30 -0
  39. package/dist/services/config/globalConfigManager.js +216 -0
  40. package/dist/services/config/index.d.ts +13 -0
  41. package/dist/services/config/index.js +13 -0
  42. package/dist/services/config/projectConfigManager.d.ts +41 -0
  43. package/dist/services/config/projectConfigManager.js +181 -0
  44. package/dist/services/config/projectConfigManager.test.d.ts +1 -0
  45. package/dist/services/config/projectConfigManager.test.js +105 -0
  46. package/dist/services/config/testUtils.d.ts +81 -0
  47. package/dist/services/config/testUtils.js +351 -0
  48. package/dist/services/sessionManager.autoApproval.test.js +9 -6
  49. package/dist/services/sessionManager.d.ts +2 -0
  50. package/dist/services/sessionManager.effect.test.js +27 -18
  51. package/dist/services/sessionManager.js +43 -40
  52. package/dist/services/sessionManager.statePersistence.test.js +5 -4
  53. package/dist/services/sessionManager.test.js +71 -49
  54. package/dist/services/shortcutManager.d.ts +0 -1
  55. package/dist/services/shortcutManager.js +5 -16
  56. package/dist/services/shortcutManager.test.js +2 -2
  57. package/dist/services/stateDetector/base.d.ts +1 -0
  58. package/dist/services/stateDetector/claude.d.ts +1 -0
  59. package/dist/services/stateDetector/claude.js +8 -0
  60. package/dist/services/stateDetector/claude.test.js +102 -0
  61. package/dist/services/stateDetector/cline.d.ts +1 -0
  62. package/dist/services/stateDetector/cline.js +3 -0
  63. package/dist/services/stateDetector/codex.d.ts +1 -0
  64. package/dist/services/stateDetector/codex.js +3 -0
  65. package/dist/services/stateDetector/cursor.d.ts +1 -0
  66. package/dist/services/stateDetector/cursor.js +3 -0
  67. package/dist/services/stateDetector/gemini.d.ts +1 -0
  68. package/dist/services/stateDetector/gemini.js +3 -0
  69. package/dist/services/stateDetector/github-copilot.d.ts +1 -0
  70. package/dist/services/stateDetector/github-copilot.js +3 -0
  71. package/dist/services/stateDetector/opencode.d.ts +1 -0
  72. package/dist/services/stateDetector/opencode.js +3 -0
  73. package/dist/services/stateDetector/types.d.ts +1 -0
  74. package/dist/services/worktreeService.d.ts +12 -0
  75. package/dist/services/worktreeService.js +24 -4
  76. package/dist/services/worktreeService.sort.test.js +105 -109
  77. package/dist/services/worktreeService.test.js +5 -5
  78. package/dist/types/index.d.ts +47 -7
  79. package/dist/utils/gitUtils.d.ts +8 -0
  80. package/dist/utils/gitUtils.js +32 -0
  81. package/dist/utils/hookExecutor.js +2 -2
  82. package/dist/utils/hookExecutor.test.js +13 -12
  83. package/dist/utils/mutex.d.ts +1 -0
  84. package/dist/utils/mutex.js +1 -0
  85. package/dist/utils/worktreeUtils.js +3 -2
  86. package/dist/utils/worktreeUtils.test.js +2 -1
  87. package/package.json +7 -7
  88. package/dist/services/configurationManager.d.ts +0 -121
  89. package/dist/services/configurationManager.js +0 -597
  90. /package/dist/{services/configurationManager.effect.test.d.ts → constants/statusIcons.test.d.ts} +0 -0
  91. /package/dist/services/{configurationManager.selectPresetOnStart.test.d.ts → config/configEditor.effect.test.d.ts} +0 -0
  92. /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 { ConfigurationManager } from './configurationManager.js';
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('ConfigurationManager - Command Presets', () => {
16
- let configManager;
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 JSON.stringify(mockConfigData);
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
- // Create new instance for each test
42
- configManager = new ConfigurationManager();
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
- // Remove command config for this test
50
- delete mockConfigData.command;
51
- configManager = new ConfigurationManager();
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
- configManager = new ConfigurationManager();
71
- const presets = configManager.getCommandPresets();
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
- configManager.setCommandPresets(newPresets);
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
- configManager = new ConfigurationManager();
121
- const defaultPreset = configManager.getDefaultPreset();
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
- configManager = new ConfigurationManager();
138
- const defaultPreset = configManager.getDefaultPreset();
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
- configManager = new ConfigurationManager();
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
- configManager.addPreset(newPreset);
188
- const presets = configManager.getCommandPresets();
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
- configManager = new ConfigurationManager();
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
- configManager.addPreset(updatedPreset);
205
- const presets = configManager.getCommandPresets();
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
- configManager = new ConfigurationManager();
220
- configManager.deletePreset('2');
221
- const presets = configManager.getCommandPresets();
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
- configManager = new ConfigurationManager();
231
- configManager.deletePreset('1');
232
- const presets = configManager.getCommandPresets();
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
- configManager = new ConfigurationManager();
244
- configManager.deletePreset('2');
245
- const presets = configManager.getCommandPresets();
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
- configManager = new ConfigurationManager();
259
- configManager.setDefaultPreset('2');
260
- const presets = configManager.getCommandPresets();
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
- configManager = new ConfigurationManager();
269
- configManager.setDefaultPreset('999');
270
- const presets = configManager.getCommandPresets();
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,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 {};