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
@@ -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,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 {};