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
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo } from 'react';
|
|
2
|
+
import { ConfigEditor } from '../services/config/configEditor.js';
|
|
3
|
+
const ConfigEditorContext = createContext(null);
|
|
4
|
+
/**
|
|
5
|
+
* Provider component for ConfigEditor context.
|
|
6
|
+
* Creates a ConfigEditor instance based on scope.
|
|
7
|
+
* Uses singleton config editors to ensure config changes are
|
|
8
|
+
* immediately visible to all components.
|
|
9
|
+
*/
|
|
10
|
+
export function ConfigEditorProvider({ scope, children, }) {
|
|
11
|
+
const configEditor = useMemo(() => new ConfigEditor(scope), [scope]);
|
|
12
|
+
return (React.createElement(ConfigEditorContext.Provider, { value: configEditor }, children));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Hook to access ConfigEditor from context.
|
|
16
|
+
* Must be used within a ConfigEditorProvider.
|
|
17
|
+
*/
|
|
18
|
+
export function useConfigEditor() {
|
|
19
|
+
const context = useContext(ConfigEditorContext);
|
|
20
|
+
if (context === null) {
|
|
21
|
+
throw new Error('useConfigEditor must be used within a ConfigEditorProvider');
|
|
22
|
+
}
|
|
23
|
+
return context;
|
|
24
|
+
}
|
|
25
|
+
export { ConfigEditorContext };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Effect } from 'effect';
|
|
2
2
|
import { ProcessError } from '../types/errors.js';
|
|
3
|
-
import {
|
|
3
|
+
import { configReader } from './config/configReader.js';
|
|
4
4
|
import { logger } from '../utils/logger.js';
|
|
5
5
|
import { execFile, spawn, } from 'child_process';
|
|
6
6
|
const DEFAULT_TIMEOUT_SECONDS = 30;
|
|
7
7
|
const getTimeoutMs = () => {
|
|
8
|
-
const config =
|
|
8
|
+
const config = configReader.getAutoApprovalConfig();
|
|
9
9
|
const timeoutSeconds = config.timeout ?? DEFAULT_TIMEOUT_SECONDS;
|
|
10
10
|
return timeoutSeconds * 1000;
|
|
11
11
|
};
|
|
@@ -222,7 +222,7 @@ export class AutoApprovalVerifier {
|
|
|
222
222
|
verifyNeedsPermission(terminalOutput, options) {
|
|
223
223
|
const attemptVerification = Effect.tryPromise({
|
|
224
224
|
try: async () => {
|
|
225
|
-
const autoApprovalConfig =
|
|
225
|
+
const autoApprovalConfig = configReader.getAutoApprovalConfig();
|
|
226
226
|
const customCommand = autoApprovalConfig.customCommand?.trim();
|
|
227
227
|
const prompt = buildPrompt(terminalOutput);
|
|
228
228
|
const jsonSchema = JSON.stringify({
|
|
@@ -5,8 +5,8 @@ const execFileMock = vi.fn();
|
|
|
5
5
|
vi.mock('child_process', () => ({
|
|
6
6
|
execFile: (...args) => execFileMock(...args),
|
|
7
7
|
}));
|
|
8
|
-
vi.mock('./
|
|
9
|
-
|
|
8
|
+
vi.mock('./config/configReader.js', () => ({
|
|
9
|
+
configReader: {
|
|
10
10
|
getAutoApprovalConfig: vi.fn().mockReturnValue({ enabled: false }),
|
|
11
11
|
},
|
|
12
12
|
}));
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ConfigScope, ProjectConfigurationData, ShortcutConfig, StatusHookConfig, WorktreeHookConfig, WorktreeConfig, CommandPresetsConfig, IConfigEditor, AutoApprovalConfig } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* ConfigEditor provides scope-aware configuration editing.
|
|
4
|
+
* The scope is determined at construction time.
|
|
5
|
+
*
|
|
6
|
+
* - When scope='global', uses GlobalConfigManager singleton
|
|
7
|
+
* - When scope='project', uses ProjectConfigManager singleton
|
|
8
|
+
* (with fallback to global if project value is undefined)
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: Uses singletons to ensure that config changes are
|
|
11
|
+
* immediately visible to all components (e.g., shortcutManager, configReader).
|
|
12
|
+
*/
|
|
13
|
+
export declare class ConfigEditor implements IConfigEditor {
|
|
14
|
+
private scope;
|
|
15
|
+
private configEditor;
|
|
16
|
+
constructor(scope: ConfigScope);
|
|
17
|
+
getShortcuts(): ShortcutConfig | undefined;
|
|
18
|
+
setShortcuts(value: ShortcutConfig): void;
|
|
19
|
+
getStatusHooks(): StatusHookConfig | undefined;
|
|
20
|
+
setStatusHooks(value: StatusHookConfig): void;
|
|
21
|
+
getWorktreeHooks(): WorktreeHookConfig | undefined;
|
|
22
|
+
setWorktreeHooks(value: WorktreeHookConfig): void;
|
|
23
|
+
getWorktreeConfig(): WorktreeConfig | undefined;
|
|
24
|
+
setWorktreeConfig(value: WorktreeConfig): void;
|
|
25
|
+
getCommandPresets(): CommandPresetsConfig | undefined;
|
|
26
|
+
setCommandPresets(value: CommandPresetsConfig): void;
|
|
27
|
+
getAutoApprovalConfig(): AutoApprovalConfig | undefined;
|
|
28
|
+
setAutoApprovalConfig(value: AutoApprovalConfig): void;
|
|
29
|
+
reload(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Get the current scope
|
|
32
|
+
*/
|
|
33
|
+
getScope(): ConfigScope;
|
|
34
|
+
/**
|
|
35
|
+
* Check if project has an override for a specific field
|
|
36
|
+
*/
|
|
37
|
+
hasProjectOverride(field: keyof ProjectConfigurationData): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Remove project override for a specific field
|
|
40
|
+
*/
|
|
41
|
+
removeProjectOverride(field: keyof ProjectConfigurationData): void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Factory function to create a ConfigEditor instance
|
|
45
|
+
*/
|
|
46
|
+
export declare function createConfigEditor(scope: ConfigScope): ConfigEditor;
|
package/dist/services/{configurationManager.effect.test.js → config/configEditor.effect.test.js}
RENAMED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
2
|
import { Effect, Either } from 'effect';
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
-
import {
|
|
4
|
+
import { ConfigEditor } from './configEditor.js';
|
|
5
|
+
import { loadConfigEffect, validateConfig, saveConfigEffect, setShortcutsEffect, setCommandPresetsEffect, addPresetEffect, deletePresetEffect, setDefaultPresetEffect, getPresetByIdEffect, } from './testUtils.js';
|
|
6
|
+
// Test paths (matching what GlobalConfigManager constructs with mocked homedir)
|
|
7
|
+
const TEST_CONFIG_PATH = '/home/test/.config/ccmanager/config.json';
|
|
8
|
+
const TEST_LEGACY_SHORTCUTS_PATH = '/home/test/.config/ccmanager/shortcuts.json';
|
|
5
9
|
// Mock fs module
|
|
6
10
|
vi.mock('fs', () => ({
|
|
7
11
|
existsSync: vi.fn(),
|
|
@@ -13,8 +17,8 @@ vi.mock('fs', () => ({
|
|
|
13
17
|
vi.mock('os', () => ({
|
|
14
18
|
homedir: vi.fn(() => '/home/test'),
|
|
15
19
|
}));
|
|
16
|
-
describe('
|
|
17
|
-
let
|
|
20
|
+
describe('ConfigEditor (global scope) - Effect-based operations', () => {
|
|
21
|
+
let configEditor;
|
|
18
22
|
let mockConfigData;
|
|
19
23
|
beforeEach(() => {
|
|
20
24
|
// Reset all mocks
|
|
@@ -25,10 +29,6 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
25
29
|
returnToMenu: { ctrl: true, key: 'e' },
|
|
26
30
|
cancel: { key: 'escape' },
|
|
27
31
|
},
|
|
28
|
-
command: {
|
|
29
|
-
command: 'claude',
|
|
30
|
-
args: ['--existing'],
|
|
31
|
-
},
|
|
32
32
|
};
|
|
33
33
|
// Mock file system operations
|
|
34
34
|
existsSync.mockImplementation((path) => {
|
|
@@ -39,25 +39,24 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
39
39
|
});
|
|
40
40
|
mkdirSync.mockImplementation(() => { });
|
|
41
41
|
writeFileSync.mockImplementation(() => { });
|
|
42
|
-
// Create new instance for each test
|
|
43
|
-
|
|
42
|
+
// Create new instance for each test and reload to pick up mocked fs
|
|
43
|
+
configEditor = new ConfigEditor('global');
|
|
44
|
+
configEditor.reload();
|
|
44
45
|
});
|
|
45
46
|
afterEach(() => {
|
|
46
47
|
vi.resetAllMocks();
|
|
47
48
|
});
|
|
48
49
|
describe('loadConfigEffect', () => {
|
|
49
50
|
it('should return Effect with ConfigurationData on success', async () => {
|
|
50
|
-
const result = await Effect.runPromise(
|
|
51
|
+
const result = await Effect.runPromise(loadConfigEffect(TEST_CONFIG_PATH, TEST_LEGACY_SHORTCUTS_PATH));
|
|
51
52
|
expect(result).toBeDefined();
|
|
52
53
|
expect(result.shortcuts).toBeDefined();
|
|
53
|
-
expect(result.command).toBeDefined();
|
|
54
54
|
});
|
|
55
55
|
it('should fail with FileSystemError when file read fails', async () => {
|
|
56
56
|
readFileSync.mockImplementation(() => {
|
|
57
57
|
throw new Error('EACCES: permission denied');
|
|
58
58
|
});
|
|
59
|
-
|
|
60
|
-
const result = await Effect.runPromise(Effect.either(configManager.loadConfigEffect()));
|
|
59
|
+
const result = await Effect.runPromise(Effect.either(loadConfigEffect(TEST_CONFIG_PATH, TEST_LEGACY_SHORTCUTS_PATH)));
|
|
61
60
|
expect(Either.isLeft(result)).toBe(true);
|
|
62
61
|
if (Either.isLeft(result)) {
|
|
63
62
|
expect(result.left._tag).toBe('FileSystemError');
|
|
@@ -69,8 +68,7 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
69
68
|
readFileSync.mockImplementation(() => {
|
|
70
69
|
return 'invalid json{';
|
|
71
70
|
});
|
|
72
|
-
|
|
73
|
-
const result = await Effect.runPromise(Effect.either(configManager.loadConfigEffect()));
|
|
71
|
+
const result = await Effect.runPromise(Effect.either(loadConfigEffect(TEST_CONFIG_PATH, TEST_LEGACY_SHORTCUTS_PATH)));
|
|
74
72
|
expect(Either.isLeft(result)).toBe(true);
|
|
75
73
|
if (Either.isLeft(result)) {
|
|
76
74
|
expect(result.left._tag).toBe('ConfigError');
|
|
@@ -94,8 +92,7 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
94
92
|
}
|
|
95
93
|
return '{}';
|
|
96
94
|
});
|
|
97
|
-
|
|
98
|
-
const result = await Effect.runPromise(configManager.loadConfigEffect());
|
|
95
|
+
const result = await Effect.runPromise(loadConfigEffect(TEST_CONFIG_PATH, TEST_LEGACY_SHORTCUTS_PATH));
|
|
99
96
|
expect(result.shortcuts).toEqual(legacyShortcuts);
|
|
100
97
|
expect(writeFileSync).toHaveBeenCalled();
|
|
101
98
|
});
|
|
@@ -108,7 +105,7 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
108
105
|
cancel: { key: 'escape' },
|
|
109
106
|
},
|
|
110
107
|
};
|
|
111
|
-
await Effect.runPromise(
|
|
108
|
+
await Effect.runPromise(saveConfigEffect(configEditor, newConfig, TEST_CONFIG_PATH));
|
|
112
109
|
expect(writeFileSync).toHaveBeenCalledWith(expect.stringContaining('config.json'), expect.any(String));
|
|
113
110
|
});
|
|
114
111
|
it('should fail with FileSystemError when file write fails', async () => {
|
|
@@ -121,7 +118,7 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
121
118
|
cancel: { key: 'escape' },
|
|
122
119
|
},
|
|
123
120
|
};
|
|
124
|
-
const result = await Effect.runPromise(Effect.either(
|
|
121
|
+
const result = await Effect.runPromise(Effect.either(saveConfigEffect(configEditor, newConfig, TEST_CONFIG_PATH)));
|
|
125
122
|
expect(Either.isLeft(result)).toBe(true);
|
|
126
123
|
if (Either.isLeft(result)) {
|
|
127
124
|
expect(result.left._tag).toBe('FileSystemError');
|
|
@@ -138,7 +135,7 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
138
135
|
cancel: { key: 'escape' },
|
|
139
136
|
},
|
|
140
137
|
};
|
|
141
|
-
const result =
|
|
138
|
+
const result = validateConfig(validConfig);
|
|
142
139
|
expect(Either.isRight(result)).toBe(true);
|
|
143
140
|
if (Either.isRight(result)) {
|
|
144
141
|
expect(result.right).toEqual(validConfig);
|
|
@@ -148,7 +145,7 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
148
145
|
const invalidConfig = {
|
|
149
146
|
shortcuts: 'not an object',
|
|
150
147
|
};
|
|
151
|
-
const result =
|
|
148
|
+
const result = validateConfig(invalidConfig);
|
|
152
149
|
expect(Either.isLeft(result)).toBe(true);
|
|
153
150
|
if (Either.isLeft(result)) {
|
|
154
151
|
const error = result.left;
|
|
@@ -157,7 +154,7 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
157
154
|
}
|
|
158
155
|
});
|
|
159
156
|
it('should return Left for null config', () => {
|
|
160
|
-
const result =
|
|
157
|
+
const result = validateConfig(null);
|
|
161
158
|
expect(Either.isLeft(result)).toBe(true);
|
|
162
159
|
if (Either.isLeft(result)) {
|
|
163
160
|
const error = result.left;
|
|
@@ -174,8 +171,8 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
174
171
|
],
|
|
175
172
|
defaultPresetId: '1',
|
|
176
173
|
};
|
|
177
|
-
|
|
178
|
-
const result =
|
|
174
|
+
configEditor.reload();
|
|
175
|
+
const result = getPresetByIdEffect(configEditor, '2');
|
|
179
176
|
expect(Either.isRight(result)).toBe(true);
|
|
180
177
|
if (Either.isRight(result)) {
|
|
181
178
|
expect(result.right).toEqual({
|
|
@@ -191,8 +188,8 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
191
188
|
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
192
189
|
defaultPresetId: '1',
|
|
193
190
|
};
|
|
194
|
-
|
|
195
|
-
const result =
|
|
191
|
+
configEditor.reload();
|
|
192
|
+
const result = getPresetByIdEffect(configEditor, '999');
|
|
196
193
|
expect(Either.isLeft(result)).toBe(true);
|
|
197
194
|
if (Either.isLeft(result)) {
|
|
198
195
|
const error = result.left;
|
|
@@ -206,8 +203,8 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
206
203
|
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
207
204
|
defaultPresetId: '1',
|
|
208
205
|
};
|
|
209
|
-
|
|
210
|
-
const result =
|
|
206
|
+
configEditor.reload();
|
|
207
|
+
const result = getPresetByIdEffect(configEditor, 'invalid-id');
|
|
211
208
|
expect(Either.isLeft(result)).toBe(true);
|
|
212
209
|
if (Either.isLeft(result)) {
|
|
213
210
|
const error = result.left;
|
|
@@ -221,7 +218,7 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
221
218
|
returnToMenu: { ctrl: true, key: 'z' },
|
|
222
219
|
cancel: { key: 'escape' },
|
|
223
220
|
};
|
|
224
|
-
await Effect.runPromise(
|
|
221
|
+
await Effect.runPromise(setShortcutsEffect(configEditor, newShortcuts, TEST_CONFIG_PATH));
|
|
225
222
|
expect(writeFileSync).toHaveBeenCalled();
|
|
226
223
|
});
|
|
227
224
|
it('should fail with FileSystemError when save fails', async () => {
|
|
@@ -232,7 +229,7 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
232
229
|
returnToMenu: { ctrl: true, key: 'z' },
|
|
233
230
|
cancel: { key: 'escape' },
|
|
234
231
|
};
|
|
235
|
-
const result = await Effect.runPromise(Effect.either(
|
|
232
|
+
const result = await Effect.runPromise(Effect.either(setShortcutsEffect(configEditor, newShortcuts, TEST_CONFIG_PATH)));
|
|
236
233
|
expect(Either.isLeft(result)).toBe(true);
|
|
237
234
|
if (Either.isLeft(result)) {
|
|
238
235
|
expect(result.left._tag).toBe('FileSystemError');
|
|
@@ -248,7 +245,7 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
248
245
|
],
|
|
249
246
|
defaultPresetId: '2',
|
|
250
247
|
};
|
|
251
|
-
await Effect.runPromise(
|
|
248
|
+
await Effect.runPromise(setCommandPresetsEffect(configEditor, newPresets, TEST_CONFIG_PATH));
|
|
252
249
|
expect(writeFileSync).toHaveBeenCalled();
|
|
253
250
|
});
|
|
254
251
|
it('should fail with FileSystemError when save fails', async () => {
|
|
@@ -259,7 +256,7 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
259
256
|
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
260
257
|
defaultPresetId: '1',
|
|
261
258
|
};
|
|
262
|
-
const result = await Effect.runPromise(Effect.either(
|
|
259
|
+
const result = await Effect.runPromise(Effect.either(setCommandPresetsEffect(configEditor, newPresets, TEST_CONFIG_PATH)));
|
|
263
260
|
expect(Either.isLeft(result)).toBe(true);
|
|
264
261
|
if (Either.isLeft(result)) {
|
|
265
262
|
expect(result.left._tag).toBe('FileSystemError');
|
|
@@ -272,14 +269,14 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
272
269
|
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
273
270
|
defaultPresetId: '1',
|
|
274
271
|
};
|
|
275
|
-
|
|
272
|
+
configEditor.reload();
|
|
276
273
|
const newPreset = {
|
|
277
274
|
id: '2',
|
|
278
275
|
name: 'New',
|
|
279
276
|
command: 'claude',
|
|
280
277
|
args: ['--new'],
|
|
281
278
|
};
|
|
282
|
-
await Effect.runPromise(
|
|
279
|
+
await Effect.runPromise(addPresetEffect(configEditor, newPreset, TEST_CONFIG_PATH));
|
|
283
280
|
expect(writeFileSync).toHaveBeenCalled();
|
|
284
281
|
});
|
|
285
282
|
it('should replace existing preset with same id', async () => {
|
|
@@ -287,14 +284,14 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
287
284
|
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
288
285
|
defaultPresetId: '1',
|
|
289
286
|
};
|
|
290
|
-
|
|
287
|
+
configEditor.reload();
|
|
291
288
|
const updatedPreset = {
|
|
292
289
|
id: '1',
|
|
293
290
|
name: 'Updated',
|
|
294
291
|
command: 'claude',
|
|
295
292
|
args: ['--updated'],
|
|
296
293
|
};
|
|
297
|
-
await Effect.runPromise(
|
|
294
|
+
await Effect.runPromise(addPresetEffect(configEditor, updatedPreset, TEST_CONFIG_PATH));
|
|
298
295
|
expect(writeFileSync).toHaveBeenCalled();
|
|
299
296
|
});
|
|
300
297
|
it('should fail with FileSystemError when save fails', async () => {
|
|
@@ -306,7 +303,7 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
306
303
|
name: 'New',
|
|
307
304
|
command: 'claude',
|
|
308
305
|
};
|
|
309
|
-
const result = await Effect.runPromise(Effect.either(
|
|
306
|
+
const result = await Effect.runPromise(Effect.either(addPresetEffect(configEditor, newPreset, TEST_CONFIG_PATH)));
|
|
310
307
|
expect(Either.isLeft(result)).toBe(true);
|
|
311
308
|
if (Either.isLeft(result)) {
|
|
312
309
|
expect(result.left._tag).toBe('FileSystemError');
|
|
@@ -322,8 +319,8 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
322
319
|
],
|
|
323
320
|
defaultPresetId: '1',
|
|
324
321
|
};
|
|
325
|
-
|
|
326
|
-
await Effect.runPromise(
|
|
322
|
+
configEditor.reload();
|
|
323
|
+
await Effect.runPromise(deletePresetEffect(configEditor, '2', TEST_CONFIG_PATH));
|
|
327
324
|
expect(writeFileSync).toHaveBeenCalled();
|
|
328
325
|
});
|
|
329
326
|
it('should fail with ValidationError when deleting last preset', async () => {
|
|
@@ -331,8 +328,8 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
331
328
|
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
332
329
|
defaultPresetId: '1',
|
|
333
330
|
};
|
|
334
|
-
|
|
335
|
-
const result = await Effect.runPromise(Effect.either(
|
|
331
|
+
configEditor.reload();
|
|
332
|
+
const result = await Effect.runPromise(Effect.either(deletePresetEffect(configEditor, '1', TEST_CONFIG_PATH)));
|
|
336
333
|
expect(Either.isLeft(result)).toBe(true);
|
|
337
334
|
if (Either.isLeft(result)) {
|
|
338
335
|
expect(result.left._tag).toBe('ValidationError');
|
|
@@ -348,11 +345,11 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
348
345
|
],
|
|
349
346
|
defaultPresetId: '1',
|
|
350
347
|
};
|
|
351
|
-
|
|
348
|
+
configEditor.reload();
|
|
352
349
|
writeFileSync.mockImplementation(() => {
|
|
353
350
|
throw new Error('Save failed');
|
|
354
351
|
});
|
|
355
|
-
const result = await Effect.runPromise(Effect.either(
|
|
352
|
+
const result = await Effect.runPromise(Effect.either(deletePresetEffect(configEditor, '2', TEST_CONFIG_PATH)));
|
|
356
353
|
expect(Either.isLeft(result)).toBe(true);
|
|
357
354
|
if (Either.isLeft(result)) {
|
|
358
355
|
expect(result.left._tag).toBe('FileSystemError');
|
|
@@ -368,8 +365,8 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
368
365
|
],
|
|
369
366
|
defaultPresetId: '1',
|
|
370
367
|
};
|
|
371
|
-
|
|
372
|
-
await Effect.runPromise(
|
|
368
|
+
configEditor.reload();
|
|
369
|
+
await Effect.runPromise(setDefaultPresetEffect(configEditor, '2', TEST_CONFIG_PATH));
|
|
373
370
|
expect(writeFileSync).toHaveBeenCalled();
|
|
374
371
|
});
|
|
375
372
|
it('should fail with ValidationError when preset id does not exist', async () => {
|
|
@@ -377,8 +374,8 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
377
374
|
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
378
375
|
defaultPresetId: '1',
|
|
379
376
|
};
|
|
380
|
-
|
|
381
|
-
const result = await Effect.runPromise(Effect.either(
|
|
377
|
+
configEditor.reload();
|
|
378
|
+
const result = await Effect.runPromise(Effect.either(setDefaultPresetEffect(configEditor, '999', TEST_CONFIG_PATH)));
|
|
382
379
|
expect(Either.isLeft(result)).toBe(true);
|
|
383
380
|
if (Either.isLeft(result)) {
|
|
384
381
|
expect(result.left._tag).toBe('ValidationError');
|
|
@@ -393,11 +390,11 @@ describe('ConfigurationManager - Effect-based operations', () => {
|
|
|
393
390
|
],
|
|
394
391
|
defaultPresetId: '1',
|
|
395
392
|
};
|
|
396
|
-
|
|
393
|
+
configEditor.reload();
|
|
397
394
|
writeFileSync.mockImplementation(() => {
|
|
398
395
|
throw new Error('Save failed');
|
|
399
396
|
});
|
|
400
|
-
const result = await Effect.runPromise(Effect.either(
|
|
397
|
+
const result = await Effect.runPromise(Effect.either(setDefaultPresetEffect(configEditor, '2', TEST_CONFIG_PATH)));
|
|
401
398
|
expect(Either.isLeft(result)).toBe(true);
|
|
402
399
|
if (Either.isLeft(result)) {
|
|
403
400
|
expect(result.left._tag).toBe('FileSystemError');
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { globalConfigManager } from './globalConfigManager.js';
|
|
2
|
+
import { projectConfigManager } from './projectConfigManager.js';
|
|
3
|
+
/**
|
|
4
|
+
* ConfigEditor provides scope-aware configuration editing.
|
|
5
|
+
* The scope is determined at construction time.
|
|
6
|
+
*
|
|
7
|
+
* - When scope='global', uses GlobalConfigManager singleton
|
|
8
|
+
* - When scope='project', uses ProjectConfigManager singleton
|
|
9
|
+
* (with fallback to global if project value is undefined)
|
|
10
|
+
*
|
|
11
|
+
* IMPORTANT: Uses singletons to ensure that config changes are
|
|
12
|
+
* immediately visible to all components (e.g., shortcutManager, configReader).
|
|
13
|
+
*/
|
|
14
|
+
export class ConfigEditor {
|
|
15
|
+
constructor(scope) {
|
|
16
|
+
Object.defineProperty(this, "scope", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true,
|
|
20
|
+
value: void 0
|
|
21
|
+
});
|
|
22
|
+
Object.defineProperty(this, "configEditor", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: void 0
|
|
27
|
+
});
|
|
28
|
+
this.scope = scope;
|
|
29
|
+
this.configEditor =
|
|
30
|
+
scope === 'global' ? globalConfigManager : projectConfigManager;
|
|
31
|
+
}
|
|
32
|
+
// IConfigEditor implementation - delegates to configEditor with fallback to global
|
|
33
|
+
getShortcuts() {
|
|
34
|
+
return (this.configEditor.getShortcuts() ?? globalConfigManager.getShortcuts());
|
|
35
|
+
}
|
|
36
|
+
setShortcuts(value) {
|
|
37
|
+
this.configEditor.setShortcuts(value);
|
|
38
|
+
}
|
|
39
|
+
getStatusHooks() {
|
|
40
|
+
return (this.configEditor.getStatusHooks() ?? globalConfigManager.getStatusHooks());
|
|
41
|
+
}
|
|
42
|
+
setStatusHooks(value) {
|
|
43
|
+
this.configEditor.setStatusHooks(value);
|
|
44
|
+
}
|
|
45
|
+
getWorktreeHooks() {
|
|
46
|
+
return (this.configEditor.getWorktreeHooks() ??
|
|
47
|
+
globalConfigManager.getWorktreeHooks());
|
|
48
|
+
}
|
|
49
|
+
setWorktreeHooks(value) {
|
|
50
|
+
this.configEditor.setWorktreeHooks(value);
|
|
51
|
+
}
|
|
52
|
+
getWorktreeConfig() {
|
|
53
|
+
return (this.configEditor.getWorktreeConfig() ??
|
|
54
|
+
globalConfigManager.getWorktreeConfig());
|
|
55
|
+
}
|
|
56
|
+
setWorktreeConfig(value) {
|
|
57
|
+
this.configEditor.setWorktreeConfig(value);
|
|
58
|
+
}
|
|
59
|
+
getCommandPresets() {
|
|
60
|
+
return (this.configEditor.getCommandPresets() ??
|
|
61
|
+
globalConfigManager.getCommandPresets());
|
|
62
|
+
}
|
|
63
|
+
setCommandPresets(value) {
|
|
64
|
+
this.configEditor.setCommandPresets(value);
|
|
65
|
+
}
|
|
66
|
+
getAutoApprovalConfig() {
|
|
67
|
+
return (this.configEditor.getAutoApprovalConfig() ??
|
|
68
|
+
globalConfigManager.getAutoApprovalConfig());
|
|
69
|
+
}
|
|
70
|
+
setAutoApprovalConfig(value) {
|
|
71
|
+
this.configEditor.setAutoApprovalConfig(value);
|
|
72
|
+
}
|
|
73
|
+
reload() {
|
|
74
|
+
this.configEditor.reload();
|
|
75
|
+
}
|
|
76
|
+
// Helper methods
|
|
77
|
+
/**
|
|
78
|
+
* Get the current scope
|
|
79
|
+
*/
|
|
80
|
+
getScope() {
|
|
81
|
+
return this.scope;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Check if project has an override for a specific field
|
|
85
|
+
*/
|
|
86
|
+
hasProjectOverride(field) {
|
|
87
|
+
return projectConfigManager.hasOverride(field);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Remove project override for a specific field
|
|
91
|
+
*/
|
|
92
|
+
removeProjectOverride(field) {
|
|
93
|
+
projectConfigManager.removeOverride(field);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Factory function to create a ConfigEditor instance
|
|
98
|
+
*/
|
|
99
|
+
export function createConfigEditor(scope) {
|
|
100
|
+
return new ConfigEditor(scope);
|
|
101
|
+
}
|
|
@@ -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 { getSelectPresetOnStart, setSelectPresetOnStart } from './testUtils.js';
|
|
4
5
|
// Mock fs module
|
|
5
6
|
vi.mock('fs', () => ({
|
|
6
7
|
existsSync: vi.fn(),
|
|
@@ -12,12 +13,14 @@ 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) - selectPresetOnStart', () => {
|
|
17
|
+
let configEditor;
|
|
17
18
|
let mockConfigData;
|
|
19
|
+
let savedConfigData = null;
|
|
18
20
|
beforeEach(() => {
|
|
19
21
|
// Reset all mocks
|
|
20
22
|
vi.clearAllMocks();
|
|
23
|
+
savedConfigData = null;
|
|
21
24
|
// Default mock config data
|
|
22
25
|
mockConfigData = {
|
|
23
26
|
shortcuts: {
|
|
@@ -46,55 +49,60 @@ describe('ConfigurationManager - selectPresetOnStart', () => {
|
|
|
46
49
|
return path.includes('config.json');
|
|
47
50
|
});
|
|
48
51
|
readFileSync.mockImplementation(() => {
|
|
49
|
-
return
|
|
52
|
+
// Return saved data if available, otherwise return initial mock data
|
|
53
|
+
return savedConfigData ?? JSON.stringify(mockConfigData);
|
|
50
54
|
});
|
|
51
55
|
mkdirSync.mockImplementation(() => { });
|
|
52
|
-
writeFileSync.mockImplementation(() => {
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
writeFileSync.mockImplementation((_path, data) => {
|
|
57
|
+
// Track written data so subsequent reads return it
|
|
58
|
+
savedConfigData = data;
|
|
59
|
+
});
|
|
60
|
+
// Create new instance for each test and reload to pick up mocked fs
|
|
61
|
+
configEditor = new ConfigEditor('global');
|
|
62
|
+
configEditor.reload();
|
|
55
63
|
});
|
|
56
64
|
afterEach(() => {
|
|
57
65
|
vi.resetAllMocks();
|
|
58
66
|
});
|
|
59
67
|
describe('getSelectPresetOnStart', () => {
|
|
60
68
|
it('should return false by default', () => {
|
|
61
|
-
const result =
|
|
69
|
+
const result = getSelectPresetOnStart(configEditor);
|
|
62
70
|
expect(result).toBe(false);
|
|
63
71
|
});
|
|
64
72
|
it('should return true when configured', () => {
|
|
65
73
|
mockConfigData.commandPresets.selectPresetOnStart = true;
|
|
66
|
-
|
|
67
|
-
const result =
|
|
74
|
+
configEditor.reload();
|
|
75
|
+
const result = getSelectPresetOnStart(configEditor);
|
|
68
76
|
expect(result).toBe(true);
|
|
69
77
|
});
|
|
70
78
|
it('should return false when explicitly set to false', () => {
|
|
71
79
|
mockConfigData.commandPresets.selectPresetOnStart = false;
|
|
72
|
-
|
|
73
|
-
const result =
|
|
80
|
+
configEditor.reload();
|
|
81
|
+
const result = getSelectPresetOnStart(configEditor);
|
|
74
82
|
expect(result).toBe(false);
|
|
75
83
|
});
|
|
76
84
|
});
|
|
77
85
|
describe('setSelectPresetOnStart', () => {
|
|
78
86
|
it('should set selectPresetOnStart to true', () => {
|
|
79
|
-
|
|
80
|
-
const result =
|
|
87
|
+
setSelectPresetOnStart(configEditor, true);
|
|
88
|
+
const result = getSelectPresetOnStart(configEditor);
|
|
81
89
|
expect(result).toBe(true);
|
|
82
90
|
// Verify that config was saved
|
|
83
91
|
expect(writeFileSync).toHaveBeenCalledWith(expect.stringContaining('config.json'), expect.stringContaining('"selectPresetOnStart": true'));
|
|
84
92
|
});
|
|
85
93
|
it('should set selectPresetOnStart to false', () => {
|
|
86
94
|
// First set to true
|
|
87
|
-
|
|
95
|
+
setSelectPresetOnStart(configEditor, true);
|
|
88
96
|
// Then set to false
|
|
89
|
-
|
|
90
|
-
const result =
|
|
97
|
+
setSelectPresetOnStart(configEditor, false);
|
|
98
|
+
const result = getSelectPresetOnStart(configEditor);
|
|
91
99
|
expect(result).toBe(false);
|
|
92
100
|
// Verify that config was saved
|
|
93
101
|
expect(writeFileSync).toHaveBeenLastCalledWith(expect.stringContaining('config.json'), expect.stringContaining('"selectPresetOnStart": false'));
|
|
94
102
|
});
|
|
95
103
|
it('should preserve other preset configuration when setting selectPresetOnStart', () => {
|
|
96
|
-
|
|
97
|
-
const presets =
|
|
104
|
+
setSelectPresetOnStart(configEditor, true);
|
|
105
|
+
const presets = configEditor.getCommandPresets();
|
|
98
106
|
expect(presets.presets).toHaveLength(2);
|
|
99
107
|
expect(presets.defaultPresetId).toBe('1');
|
|
100
108
|
expect(presets.selectPresetOnStart).toBe(true);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|