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,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
* This module is for internal use within the config directory only.
|
|
4
|
+
* External code should use ConfigEditor or ConfigReader instead.
|
|
5
|
+
*/
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
9
|
+
import { DEFAULT_SHORTCUTS, } from '../../types/index.js';
|
|
10
|
+
class GlobalConfigManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
Object.defineProperty(this, "configPath", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
writable: true,
|
|
16
|
+
value: void 0
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(this, "legacyShortcutsPath", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
writable: true,
|
|
22
|
+
value: void 0
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(this, "configDir", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
configurable: true,
|
|
27
|
+
writable: true,
|
|
28
|
+
value: void 0
|
|
29
|
+
});
|
|
30
|
+
Object.defineProperty(this, "config", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
configurable: true,
|
|
33
|
+
writable: true,
|
|
34
|
+
value: {}
|
|
35
|
+
});
|
|
36
|
+
// Determine config directory based on platform
|
|
37
|
+
const homeDir = homedir();
|
|
38
|
+
this.configDir =
|
|
39
|
+
process.platform === 'win32'
|
|
40
|
+
? join(process.env['APPDATA'] || join(homeDir, 'AppData', 'Roaming'), 'ccmanager')
|
|
41
|
+
: join(homeDir, '.config', 'ccmanager');
|
|
42
|
+
// Ensure config directory exists
|
|
43
|
+
if (!existsSync(this.configDir)) {
|
|
44
|
+
mkdirSync(this.configDir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
this.configPath = join(this.configDir, 'config.json');
|
|
47
|
+
this.legacyShortcutsPath = join(this.configDir, 'shortcuts.json');
|
|
48
|
+
this.loadConfig();
|
|
49
|
+
}
|
|
50
|
+
loadConfig() {
|
|
51
|
+
// Try to load the new config file
|
|
52
|
+
if (existsSync(this.configPath)) {
|
|
53
|
+
try {
|
|
54
|
+
const configData = readFileSync(this.configPath, 'utf-8');
|
|
55
|
+
this.config = JSON.parse(configData);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.error('Failed to load configuration:', error);
|
|
59
|
+
this.config = {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// If new config doesn't exist, check for legacy shortcuts.json
|
|
64
|
+
this.migrateLegacyShortcuts();
|
|
65
|
+
}
|
|
66
|
+
// Check if shortcuts need to be loaded from legacy file
|
|
67
|
+
// This handles the case where config.json exists but doesn't have shortcuts
|
|
68
|
+
if (!this.config.shortcuts && existsSync(this.legacyShortcutsPath)) {
|
|
69
|
+
this.migrateLegacyShortcuts();
|
|
70
|
+
}
|
|
71
|
+
// Ensure default values
|
|
72
|
+
if (!this.config.shortcuts) {
|
|
73
|
+
this.config.shortcuts = DEFAULT_SHORTCUTS;
|
|
74
|
+
}
|
|
75
|
+
if (!this.config.statusHooks) {
|
|
76
|
+
this.config.statusHooks = {};
|
|
77
|
+
}
|
|
78
|
+
if (!this.config.worktreeHooks) {
|
|
79
|
+
this.config.worktreeHooks = {};
|
|
80
|
+
}
|
|
81
|
+
if (!this.config.worktree) {
|
|
82
|
+
this.config.worktree = {
|
|
83
|
+
autoDirectory: false,
|
|
84
|
+
copySessionData: true,
|
|
85
|
+
sortByLastSession: false,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
if (!Object.prototype.hasOwnProperty.call(this.config.worktree, 'copySessionData')) {
|
|
89
|
+
this.config.worktree.copySessionData = true;
|
|
90
|
+
}
|
|
91
|
+
if (!Object.prototype.hasOwnProperty.call(this.config.worktree, 'sortByLastSession')) {
|
|
92
|
+
this.config.worktree.sortByLastSession = false;
|
|
93
|
+
}
|
|
94
|
+
if (!this.config.autoApproval) {
|
|
95
|
+
this.config.autoApproval = {
|
|
96
|
+
enabled: false,
|
|
97
|
+
timeout: 30,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
if (!Object.prototype.hasOwnProperty.call(this.config.autoApproval, 'enabled')) {
|
|
102
|
+
this.config.autoApproval.enabled = false;
|
|
103
|
+
}
|
|
104
|
+
if (!Object.prototype.hasOwnProperty.call(this.config.autoApproval, 'timeout')) {
|
|
105
|
+
this.config.autoApproval.timeout = 30;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Migrate legacy command config to presets if needed
|
|
109
|
+
this.ensureDefaultPresets();
|
|
110
|
+
}
|
|
111
|
+
migrateLegacyShortcuts() {
|
|
112
|
+
if (existsSync(this.legacyShortcutsPath)) {
|
|
113
|
+
try {
|
|
114
|
+
const shortcutsData = readFileSync(this.legacyShortcutsPath, 'utf-8');
|
|
115
|
+
const shortcuts = JSON.parse(shortcutsData);
|
|
116
|
+
// Validate that it's a valid shortcuts config
|
|
117
|
+
if (shortcuts && typeof shortcuts === 'object') {
|
|
118
|
+
this.config.shortcuts = shortcuts;
|
|
119
|
+
// Save to new config format
|
|
120
|
+
this.saveConfig();
|
|
121
|
+
console.log('Migrated shortcuts from legacy shortcuts.json to config.json');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
console.error('Failed to migrate legacy shortcuts:', error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
saveConfig() {
|
|
130
|
+
try {
|
|
131
|
+
const jsonData = JSON.stringify(this.config, null, 2);
|
|
132
|
+
writeFileSync(this.configPath, jsonData);
|
|
133
|
+
// Re-parse to ensure in-memory state matches what was written to disk
|
|
134
|
+
this.config = JSON.parse(jsonData);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
console.error('Failed to save configuration:', error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
getShortcuts() {
|
|
141
|
+
return this.config.shortcuts || DEFAULT_SHORTCUTS;
|
|
142
|
+
}
|
|
143
|
+
setShortcuts(shortcuts) {
|
|
144
|
+
this.config.shortcuts = shortcuts;
|
|
145
|
+
this.saveConfig();
|
|
146
|
+
}
|
|
147
|
+
getStatusHooks() {
|
|
148
|
+
return this.config.statusHooks || {};
|
|
149
|
+
}
|
|
150
|
+
setStatusHooks(hooks) {
|
|
151
|
+
this.config.statusHooks = hooks;
|
|
152
|
+
this.saveConfig();
|
|
153
|
+
}
|
|
154
|
+
getWorktreeHooks() {
|
|
155
|
+
return this.config.worktreeHooks || {};
|
|
156
|
+
}
|
|
157
|
+
setWorktreeHooks(hooks) {
|
|
158
|
+
this.config.worktreeHooks = hooks;
|
|
159
|
+
this.saveConfig();
|
|
160
|
+
}
|
|
161
|
+
getWorktreeConfig() {
|
|
162
|
+
return (this.config.worktree || {
|
|
163
|
+
autoDirectory: false,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
setWorktreeConfig(worktreeConfig) {
|
|
167
|
+
this.config.worktree = worktreeConfig;
|
|
168
|
+
this.saveConfig();
|
|
169
|
+
}
|
|
170
|
+
getAutoApprovalConfig() {
|
|
171
|
+
const config = this.config.autoApproval || {
|
|
172
|
+
enabled: false,
|
|
173
|
+
};
|
|
174
|
+
// Default timeout to 30 seconds if not set
|
|
175
|
+
return {
|
|
176
|
+
...config,
|
|
177
|
+
timeout: config.timeout ?? 30,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
setAutoApprovalConfig(autoApproval) {
|
|
181
|
+
this.config.autoApproval = autoApproval;
|
|
182
|
+
this.saveConfig();
|
|
183
|
+
}
|
|
184
|
+
ensureDefaultPresets() {
|
|
185
|
+
// Ensure default presets if none exist
|
|
186
|
+
if (!this.config.commandPresets) {
|
|
187
|
+
this.config.commandPresets = {
|
|
188
|
+
presets: [
|
|
189
|
+
{
|
|
190
|
+
id: '1',
|
|
191
|
+
name: 'Main',
|
|
192
|
+
command: 'claude',
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
defaultPresetId: '1',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
getCommandPresets() {
|
|
200
|
+
if (!this.config.commandPresets) {
|
|
201
|
+
this.ensureDefaultPresets();
|
|
202
|
+
}
|
|
203
|
+
return this.config.commandPresets;
|
|
204
|
+
}
|
|
205
|
+
setCommandPresets(presets) {
|
|
206
|
+
this.config.commandPresets = presets;
|
|
207
|
+
this.saveConfig();
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Reload configuration from disk
|
|
211
|
+
*/
|
|
212
|
+
reload() {
|
|
213
|
+
this.loadConfig();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export const globalConfigManager = new GlobalConfigManager();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config module - handles global and project-level configuration.
|
|
3
|
+
*
|
|
4
|
+
* Public API:
|
|
5
|
+
* - ConfigEditor: For editing configuration (scope-aware)
|
|
6
|
+
* - ConfigReader: For reading merged configuration
|
|
7
|
+
* - createConfigEditor: Factory function to create ConfigEditor instances
|
|
8
|
+
*
|
|
9
|
+
* Note: globalConfigManager and projectConfigManager are internal
|
|
10
|
+
* and should not be imported directly from outside this directory.
|
|
11
|
+
*/
|
|
12
|
+
export { ConfigEditor, createConfigEditor } from './configEditor.js';
|
|
13
|
+
export { ConfigReader, configReader } from './configReader.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config module - handles global and project-level configuration.
|
|
3
|
+
*
|
|
4
|
+
* Public API:
|
|
5
|
+
* - ConfigEditor: For editing configuration (scope-aware)
|
|
6
|
+
* - ConfigReader: For reading merged configuration
|
|
7
|
+
* - createConfigEditor: Factory function to create ConfigEditor instances
|
|
8
|
+
*
|
|
9
|
+
* Note: globalConfigManager and projectConfigManager are internal
|
|
10
|
+
* and should not be imported directly from outside this directory.
|
|
11
|
+
*/
|
|
12
|
+
export { ConfigEditor, createConfigEditor } from './configEditor.js';
|
|
13
|
+
export { ConfigReader, configReader } from './configReader.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ProjectConfigurationData, ShortcutConfig, StatusHookConfig, WorktreeHookConfig, WorktreeConfig, CommandPresetsConfig, IConfigEditor, AutoApprovalConfig } from '../../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* ProjectConfigManager handles project-specific configuration.
|
|
4
|
+
* Reads/writes from `<git repository root>/.ccmanager.json`.
|
|
5
|
+
* Implements IConfigEditor for consistent API with GlobalConfigManager.
|
|
6
|
+
*/
|
|
7
|
+
declare class ProjectConfigManager implements IConfigEditor {
|
|
8
|
+
private gitRoot;
|
|
9
|
+
private configPath;
|
|
10
|
+
private projectConfig;
|
|
11
|
+
constructor(cwd: string);
|
|
12
|
+
private loadProjectConfig;
|
|
13
|
+
private saveProjectConfig;
|
|
14
|
+
private ensureProjectConfig;
|
|
15
|
+
getShortcuts(): ShortcutConfig | undefined;
|
|
16
|
+
setShortcuts(value: ShortcutConfig): void;
|
|
17
|
+
getStatusHooks(): StatusHookConfig | undefined;
|
|
18
|
+
setStatusHooks(value: StatusHookConfig): void;
|
|
19
|
+
getWorktreeHooks(): WorktreeHookConfig | undefined;
|
|
20
|
+
setWorktreeHooks(value: WorktreeHookConfig): void;
|
|
21
|
+
getWorktreeConfig(): WorktreeConfig | undefined;
|
|
22
|
+
setWorktreeConfig(value: WorktreeConfig): void;
|
|
23
|
+
getCommandPresets(): CommandPresetsConfig | undefined;
|
|
24
|
+
setCommandPresets(value: CommandPresetsConfig): void;
|
|
25
|
+
getAutoApprovalConfig(): AutoApprovalConfig | undefined;
|
|
26
|
+
setAutoApprovalConfig(value: AutoApprovalConfig): void;
|
|
27
|
+
reload(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Check if a specific field has a project-level override
|
|
30
|
+
*/
|
|
31
|
+
hasOverride(field: keyof ProjectConfigurationData): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Remove a project-level override for a specific field
|
|
34
|
+
*/
|
|
35
|
+
removeOverride(field: keyof ProjectConfigurationData): void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Default singleton instance using current working directory
|
|
39
|
+
*/
|
|
40
|
+
export declare const projectConfigManager: ProjectConfigManager;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @internal
|
|
3
|
+
* This module is for internal use within the config directory only.
|
|
4
|
+
* External code should use ConfigEditor or ConfigReader instead.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { ENV_VARS } from '../../constants/env.js';
|
|
9
|
+
import { getGitRepositoryRoot } from '../../utils/gitUtils.js';
|
|
10
|
+
const PROJECT_CONFIG_FILENAME = '.ccmanager.json';
|
|
11
|
+
/**
|
|
12
|
+
* ProjectConfigManager handles project-specific configuration.
|
|
13
|
+
* Reads/writes from `<git repository root>/.ccmanager.json`.
|
|
14
|
+
* Implements IConfigEditor for consistent API with GlobalConfigManager.
|
|
15
|
+
*/
|
|
16
|
+
class ProjectConfigManager {
|
|
17
|
+
constructor(cwd) {
|
|
18
|
+
Object.defineProperty(this, "gitRoot", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
writable: true,
|
|
22
|
+
value: void 0
|
|
23
|
+
});
|
|
24
|
+
Object.defineProperty(this, "configPath", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
configurable: true,
|
|
27
|
+
writable: true,
|
|
28
|
+
value: void 0
|
|
29
|
+
});
|
|
30
|
+
Object.defineProperty(this, "projectConfig", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
configurable: true,
|
|
33
|
+
writable: true,
|
|
34
|
+
value: null
|
|
35
|
+
});
|
|
36
|
+
// Use git repository root
|
|
37
|
+
this.gitRoot = getGitRepositoryRoot(cwd);
|
|
38
|
+
this.configPath = this.gitRoot
|
|
39
|
+
? join(this.gitRoot, PROJECT_CONFIG_FILENAME)
|
|
40
|
+
: null;
|
|
41
|
+
this.loadProjectConfig();
|
|
42
|
+
}
|
|
43
|
+
loadProjectConfig() {
|
|
44
|
+
// In multi-project mode, skip project config to ensure global config is used
|
|
45
|
+
if (process.env[ENV_VARS.MULTI_PROJECT_ROOT]) {
|
|
46
|
+
this.projectConfig = null;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// No git repository found
|
|
50
|
+
if (!this.configPath) {
|
|
51
|
+
this.projectConfig = null;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (existsSync(this.configPath)) {
|
|
55
|
+
try {
|
|
56
|
+
const data = readFileSync(this.configPath, 'utf-8');
|
|
57
|
+
this.projectConfig = JSON.parse(data);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
this.projectConfig = null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
this.projectConfig = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
saveProjectConfig() {
|
|
68
|
+
if (this.projectConfig === null || !this.configPath) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const jsonData = JSON.stringify(this.projectConfig, null, 2);
|
|
73
|
+
writeFileSync(this.configPath, jsonData);
|
|
74
|
+
// Re-parse to ensure in-memory state matches what was written to disk
|
|
75
|
+
this.projectConfig = JSON.parse(jsonData);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Silently fail - error handling can be added later
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
ensureProjectConfig() {
|
|
82
|
+
if (this.projectConfig === null) {
|
|
83
|
+
this.projectConfig = {};
|
|
84
|
+
}
|
|
85
|
+
return this.projectConfig;
|
|
86
|
+
}
|
|
87
|
+
// IConfigEditor implementation
|
|
88
|
+
getShortcuts() {
|
|
89
|
+
return this.projectConfig?.shortcuts;
|
|
90
|
+
}
|
|
91
|
+
setShortcuts(value) {
|
|
92
|
+
const config = this.ensureProjectConfig();
|
|
93
|
+
config.shortcuts = value;
|
|
94
|
+
this.saveProjectConfig();
|
|
95
|
+
}
|
|
96
|
+
getStatusHooks() {
|
|
97
|
+
return this.projectConfig?.statusHooks;
|
|
98
|
+
}
|
|
99
|
+
setStatusHooks(value) {
|
|
100
|
+
const config = this.ensureProjectConfig();
|
|
101
|
+
config.statusHooks = value;
|
|
102
|
+
this.saveProjectConfig();
|
|
103
|
+
}
|
|
104
|
+
getWorktreeHooks() {
|
|
105
|
+
return this.projectConfig?.worktreeHooks;
|
|
106
|
+
}
|
|
107
|
+
setWorktreeHooks(value) {
|
|
108
|
+
const config = this.ensureProjectConfig();
|
|
109
|
+
config.worktreeHooks = value;
|
|
110
|
+
this.saveProjectConfig();
|
|
111
|
+
}
|
|
112
|
+
getWorktreeConfig() {
|
|
113
|
+
return this.projectConfig?.worktree;
|
|
114
|
+
}
|
|
115
|
+
setWorktreeConfig(value) {
|
|
116
|
+
const config = this.ensureProjectConfig();
|
|
117
|
+
config.worktree = value;
|
|
118
|
+
this.saveProjectConfig();
|
|
119
|
+
}
|
|
120
|
+
getCommandPresets() {
|
|
121
|
+
return this.projectConfig?.commandPresets;
|
|
122
|
+
}
|
|
123
|
+
setCommandPresets(value) {
|
|
124
|
+
const config = this.ensureProjectConfig();
|
|
125
|
+
config.commandPresets = value;
|
|
126
|
+
this.saveProjectConfig();
|
|
127
|
+
}
|
|
128
|
+
getAutoApprovalConfig() {
|
|
129
|
+
return this.projectConfig?.autoApproval;
|
|
130
|
+
}
|
|
131
|
+
setAutoApprovalConfig(value) {
|
|
132
|
+
const config = this.ensureProjectConfig();
|
|
133
|
+
config.autoApproval = value;
|
|
134
|
+
this.saveProjectConfig();
|
|
135
|
+
}
|
|
136
|
+
reload() {
|
|
137
|
+
this.loadProjectConfig();
|
|
138
|
+
}
|
|
139
|
+
// Project-specific helper methods
|
|
140
|
+
/**
|
|
141
|
+
* Check if a specific field has a project-level override
|
|
142
|
+
*/
|
|
143
|
+
hasOverride(field) {
|
|
144
|
+
if (this.projectConfig === null) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
return this.projectConfig[field] !== undefined;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Remove a project-level override for a specific field
|
|
151
|
+
*/
|
|
152
|
+
removeOverride(field) {
|
|
153
|
+
if (this.projectConfig === null || !this.configPath) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
delete this.projectConfig[field];
|
|
157
|
+
// If project config is now empty, delete the file
|
|
158
|
+
if (Object.keys(this.projectConfig).length === 0) {
|
|
159
|
+
this.projectConfig = null;
|
|
160
|
+
try {
|
|
161
|
+
if (existsSync(this.configPath)) {
|
|
162
|
+
unlinkSync(this.configPath);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Silently fail
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
this.saveProjectConfig();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Default singleton instance using current working directory
|
|
176
|
+
*/
|
|
177
|
+
export const projectConfigManager = new ProjectConfigManager(process.cwd());
|
|
178
|
+
/**
|
|
179
|
+
* @internal - Exported for testing only
|
|
180
|
+
*/
|
|
181
|
+
export { ProjectConfigManager };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
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 child_process module
|
|
13
|
+
vi.mock('child_process', () => ({
|
|
14
|
+
execSync: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
describe('ProjectConfigManager - git repository root', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
vi.resetModules();
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.resetAllMocks();
|
|
23
|
+
});
|
|
24
|
+
it('should read config from git repository root, not cwd', async () => {
|
|
25
|
+
const cwd = '/path/to/repo/subdir';
|
|
26
|
+
const gitRoot = '/path/to/repo';
|
|
27
|
+
const projectConfig = {
|
|
28
|
+
shortcuts: {
|
|
29
|
+
returnToMenu: { ctrl: true, key: 'r' },
|
|
30
|
+
cancel: { key: 'escape' },
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
// Mock git rev-parse to return the .git directory
|
|
34
|
+
execSync.mockReturnValue(`${gitRoot}/.git\n`);
|
|
35
|
+
// Mock existsSync to return true for the git root config path
|
|
36
|
+
existsSync.mockImplementation((path) => {
|
|
37
|
+
// Config exists at git root, not at cwd
|
|
38
|
+
return path === `${gitRoot}/.ccmanager.json`;
|
|
39
|
+
});
|
|
40
|
+
// Mock readFileSync to return project config
|
|
41
|
+
readFileSync.mockReturnValue(JSON.stringify(projectConfig));
|
|
42
|
+
// Dynamic import to pick up mocks
|
|
43
|
+
const { ProjectConfigManager } = await import('./projectConfigManager.js');
|
|
44
|
+
const manager = new ProjectConfigManager(cwd);
|
|
45
|
+
// Verify config was read from git root
|
|
46
|
+
const shortcuts = manager.getShortcuts();
|
|
47
|
+
expect(shortcuts).toEqual(projectConfig.shortcuts);
|
|
48
|
+
// Verify readFileSync was called with git root path
|
|
49
|
+
expect(readFileSync).toHaveBeenCalledWith(`${gitRoot}/.ccmanager.json`, 'utf-8');
|
|
50
|
+
});
|
|
51
|
+
it('should write config to git repository root, not cwd', async () => {
|
|
52
|
+
const cwd = '/path/to/repo/deep/nested/dir';
|
|
53
|
+
const gitRoot = '/path/to/repo';
|
|
54
|
+
// Mock git rev-parse
|
|
55
|
+
execSync.mockReturnValue(`${gitRoot}/.git\n`);
|
|
56
|
+
// No existing config
|
|
57
|
+
existsSync.mockReturnValue(false);
|
|
58
|
+
writeFileSync.mockImplementation(() => { });
|
|
59
|
+
const { ProjectConfigManager } = await import('./projectConfigManager.js');
|
|
60
|
+
const manager = new ProjectConfigManager(cwd);
|
|
61
|
+
// Set shortcuts to trigger a write
|
|
62
|
+
manager.setShortcuts({
|
|
63
|
+
returnToMenu: { ctrl: true, key: 'x' },
|
|
64
|
+
cancel: { key: 'q' },
|
|
65
|
+
});
|
|
66
|
+
// Verify writeFileSync was called with git root path
|
|
67
|
+
expect(writeFileSync).toHaveBeenCalledWith(`${gitRoot}/.ccmanager.json`, expect.any(String));
|
|
68
|
+
});
|
|
69
|
+
it('should handle worktree paths correctly', async () => {
|
|
70
|
+
const cwd = '/path/to/worktree';
|
|
71
|
+
const mainRepoRoot = '/path/to/main-repo';
|
|
72
|
+
const projectConfig = {
|
|
73
|
+
worktree: {
|
|
74
|
+
autoDirectory: true,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
// Mock git rev-parse to return worktree .git path
|
|
78
|
+
execSync.mockReturnValue(`${mainRepoRoot}/.git/worktrees/my-worktree\n`);
|
|
79
|
+
existsSync.mockImplementation((path) => {
|
|
80
|
+
return path === `${mainRepoRoot}/.ccmanager.json`;
|
|
81
|
+
});
|
|
82
|
+
readFileSync.mockReturnValue(JSON.stringify(projectConfig));
|
|
83
|
+
const { ProjectConfigManager } = await import('./projectConfigManager.js');
|
|
84
|
+
const manager = new ProjectConfigManager(cwd);
|
|
85
|
+
// Verify config was read from main repo root
|
|
86
|
+
const worktreeConfig = manager.getWorktreeConfig();
|
|
87
|
+
expect(worktreeConfig).toEqual(projectConfig.worktree);
|
|
88
|
+
expect(readFileSync).toHaveBeenCalledWith(`${mainRepoRoot}/.ccmanager.json`, 'utf-8');
|
|
89
|
+
});
|
|
90
|
+
it('should return undefined when not in a git repository', async () => {
|
|
91
|
+
const cwd = '/not/a/git/repo';
|
|
92
|
+
// Mock git rev-parse to throw (not a git repo)
|
|
93
|
+
execSync.mockImplementation(() => {
|
|
94
|
+
throw new Error('fatal: not a git repository');
|
|
95
|
+
});
|
|
96
|
+
const { ProjectConfigManager } = await import('./projectConfigManager.js');
|
|
97
|
+
const manager = new ProjectConfigManager(cwd);
|
|
98
|
+
// Should return undefined for all getters
|
|
99
|
+
expect(manager.getShortcuts()).toBeUndefined();
|
|
100
|
+
expect(manager.getWorktreeConfig()).toBeUndefined();
|
|
101
|
+
expect(manager.getCommandPresets()).toBeUndefined();
|
|
102
|
+
// Should not attempt to read any file
|
|
103
|
+
expect(readFileSync).not.toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Test utilities for config module
|
|
3
|
+
*
|
|
4
|
+
* WARNING: This file is intended for TEST USE ONLY.
|
|
5
|
+
* Do not import from production code.
|
|
6
|
+
*
|
|
7
|
+
* These functions provide Effect-based wrappers for testing config operations.
|
|
8
|
+
*/
|
|
9
|
+
import { Effect, Either } from 'effect';
|
|
10
|
+
import { ConfigurationData, CommandPreset, CommandPresetsConfig, ShortcutConfig, IConfigEditor } from '../../types/index.js';
|
|
11
|
+
import { FileSystemError, ConfigError, ValidationError } from '../../types/errors.js';
|
|
12
|
+
/**
|
|
13
|
+
* TEST ONLY: Load configuration from file with Effect-based error handling
|
|
14
|
+
*
|
|
15
|
+
* @param configPath - Path to the config file
|
|
16
|
+
* @param legacyShortcutsPath - Path to legacy shortcuts file for migration
|
|
17
|
+
* @returns Effect with ConfigurationData on success, errors on failure
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadConfigEffect(configPath: string, legacyShortcutsPath: string): Effect.Effect<ConfigurationData, FileSystemError | ConfigError, never>;
|
|
20
|
+
type ValidationResult = Either.Either<ConfigurationData, ValidationError>;
|
|
21
|
+
/**
|
|
22
|
+
* TEST ONLY: Validate configuration structure
|
|
23
|
+
*/
|
|
24
|
+
export declare function validateConfig(config: unknown): ValidationResult;
|
|
25
|
+
/**
|
|
26
|
+
* TEST ONLY: Add or update a preset in the config manager
|
|
27
|
+
*/
|
|
28
|
+
export declare function addPreset(configManager: IConfigEditor, preset: CommandPreset): void;
|
|
29
|
+
/**
|
|
30
|
+
* TEST ONLY: Delete a preset by ID
|
|
31
|
+
*/
|
|
32
|
+
export declare function deletePreset(configManager: IConfigEditor, id: string): void;
|
|
33
|
+
/**
|
|
34
|
+
* TEST ONLY: Set the default preset ID
|
|
35
|
+
*/
|
|
36
|
+
export declare function setDefaultPreset(configManager: IConfigEditor, id: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* TEST ONLY: Save configuration to file with Effect-based error handling
|
|
39
|
+
*/
|
|
40
|
+
export declare function saveConfigEffect(configManager: IConfigEditor, config: ConfigurationData, configPath: string): Effect.Effect<void, FileSystemError, never>;
|
|
41
|
+
/**
|
|
42
|
+
* TEST ONLY: Set shortcuts with Effect-based error handling
|
|
43
|
+
*/
|
|
44
|
+
export declare function setShortcutsEffect(configManager: IConfigEditor, shortcuts: ShortcutConfig, configPath: string): Effect.Effect<void, FileSystemError, never>;
|
|
45
|
+
/**
|
|
46
|
+
* TEST ONLY: Set command presets with Effect-based error handling
|
|
47
|
+
*/
|
|
48
|
+
export declare function setCommandPresetsEffect(configManager: IConfigEditor, presets: CommandPresetsConfig, configPath: string): Effect.Effect<void, FileSystemError, never>;
|
|
49
|
+
/**
|
|
50
|
+
* TEST ONLY: Add or update preset with Effect-based error handling
|
|
51
|
+
*/
|
|
52
|
+
export declare function addPresetEffect(configManager: IConfigEditor, preset: CommandPreset, configPath: string): Effect.Effect<void, FileSystemError, never>;
|
|
53
|
+
/**
|
|
54
|
+
* TEST ONLY: Delete preset with Effect-based error handling
|
|
55
|
+
*/
|
|
56
|
+
export declare function deletePresetEffect(configManager: IConfigEditor, id: string, configPath: string): Effect.Effect<void, ValidationError | FileSystemError, never>;
|
|
57
|
+
/**
|
|
58
|
+
* TEST ONLY: Set default preset with Effect-based error handling
|
|
59
|
+
*/
|
|
60
|
+
export declare function setDefaultPresetEffect(configManager: IConfigEditor, id: string, configPath: string): Effect.Effect<void, ValidationError | FileSystemError, never>;
|
|
61
|
+
/**
|
|
62
|
+
* TEST ONLY: Get the default preset
|
|
63
|
+
*/
|
|
64
|
+
export declare function getDefaultPreset(configManager: IConfigEditor): CommandPreset;
|
|
65
|
+
/**
|
|
66
|
+
* TEST ONLY: Get whether to select preset on start
|
|
67
|
+
*/
|
|
68
|
+
export declare function getSelectPresetOnStart(configManager: IConfigEditor): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* TEST ONLY: Set whether to select preset on start
|
|
71
|
+
*/
|
|
72
|
+
export declare function setSelectPresetOnStart(configManager: IConfigEditor, enabled: boolean): void;
|
|
73
|
+
/**
|
|
74
|
+
* TEST ONLY: Get whether auto-approval is enabled
|
|
75
|
+
*/
|
|
76
|
+
export declare function isAutoApprovalEnabled(configManager: IConfigEditor): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* TEST ONLY: Get preset by ID with Either-based error handling
|
|
79
|
+
*/
|
|
80
|
+
export declare function getPresetByIdEffect(configManager: IConfigEditor, id: string): Either.Either<CommandPreset, ValidationError>;
|
|
81
|
+
export {};
|