ccmanager 0.1.15 → 0.2.1

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 (38) hide show
  1. package/dist/cli.js +3 -0
  2. package/dist/components/App.js +35 -1
  3. package/dist/components/ConfigureCommand.js +367 -121
  4. package/dist/components/Menu.js +18 -18
  5. package/dist/components/PresetSelector.d.ts +7 -0
  6. package/dist/components/PresetSelector.js +52 -0
  7. package/dist/hooks/useGitStatus.d.ts +2 -0
  8. package/dist/hooks/useGitStatus.js +52 -0
  9. package/dist/hooks/useGitStatus.test.d.ts +1 -0
  10. package/dist/hooks/useGitStatus.test.js +186 -0
  11. package/dist/services/configurationManager.d.ts +11 -1
  12. package/dist/services/configurationManager.js +111 -3
  13. package/dist/services/configurationManager.selectPresetOnStart.test.d.ts +1 -0
  14. package/dist/services/configurationManager.selectPresetOnStart.test.js +103 -0
  15. package/dist/services/configurationManager.test.d.ts +1 -0
  16. package/dist/services/configurationManager.test.js +313 -0
  17. package/dist/services/sessionManager.d.ts +1 -0
  18. package/dist/services/sessionManager.js +69 -0
  19. package/dist/services/sessionManager.test.js +103 -0
  20. package/dist/services/worktreeConfigManager.d.ts +10 -0
  21. package/dist/services/worktreeConfigManager.js +27 -0
  22. package/dist/services/worktreeService.js +8 -0
  23. package/dist/services/worktreeService.test.js +8 -0
  24. package/dist/types/index.d.ts +16 -0
  25. package/dist/utils/concurrencyLimit.d.ts +4 -0
  26. package/dist/utils/concurrencyLimit.js +30 -0
  27. package/dist/utils/concurrencyLimit.test.d.ts +1 -0
  28. package/dist/utils/concurrencyLimit.test.js +63 -0
  29. package/dist/utils/gitStatus.d.ts +19 -0
  30. package/dist/utils/gitStatus.js +146 -0
  31. package/dist/utils/gitStatus.test.d.ts +1 -0
  32. package/dist/utils/gitStatus.test.js +141 -0
  33. package/dist/utils/worktreeConfig.d.ts +3 -0
  34. package/dist/utils/worktreeConfig.js +43 -0
  35. package/dist/utils/worktreeUtils.d.ts +37 -0
  36. package/dist/utils/worktreeUtils.js +114 -0
  37. package/dist/utils/worktreeUtils.test.js +105 -1
  38. package/package.json +1 -1
@@ -2,16 +2,21 @@ import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import SelectInput from 'ink-select-input';
4
4
  import { WorktreeService } from '../services/worktreeService.js';
5
- import { STATUS_ICONS, STATUS_LABELS, MENU_ICONS, getStatusDisplay, } from '../constants/statusIcons.js';
5
+ import { STATUS_ICONS, STATUS_LABELS, MENU_ICONS, } from '../constants/statusIcons.js';
6
+ import { useGitStatus } from '../hooks/useGitStatus.js';
7
+ import { prepareWorktreeItems, calculateColumnPositions, assembleWorktreeLabel, } from '../utils/worktreeUtils.js';
6
8
  const Menu = ({ sessionManager, onSelectWorktree }) => {
7
- const [worktrees, setWorktrees] = useState([]);
9
+ const [baseWorktrees, setBaseWorktrees] = useState([]);
10
+ const [defaultBranch, setDefaultBranch] = useState(null);
11
+ const worktrees = useGitStatus(baseWorktrees, defaultBranch);
8
12
  const [sessions, setSessions] = useState([]);
9
13
  const [items, setItems] = useState([]);
10
14
  useEffect(() => {
11
15
  // Load worktrees
12
16
  const worktreeService = new WorktreeService();
13
17
  const loadedWorktrees = worktreeService.getWorktrees();
14
- setWorktrees(loadedWorktrees);
18
+ setBaseWorktrees(loadedWorktrees);
19
+ setDefaultBranch(worktreeService.getDefaultBranch());
15
20
  // Update sessions
16
21
  const updateSessions = () => {
17
22
  const allSessions = sessionManager.getAllSessions();
@@ -34,23 +39,18 @@ const Menu = ({ sessionManager, onSelectWorktree }) => {
34
39
  };
35
40
  }, [sessionManager]);
36
41
  useEffect(() => {
37
- // Build menu items
38
- const menuItems = worktrees.map((wt, index) => {
39
- const session = sessions.find(s => s.worktreePath === wt.path);
40
- let status = '';
41
- if (session) {
42
- status = ` [${getStatusDisplay(session.state)}]`;
43
- }
44
- const branchName = wt.branch
45
- ? wt.branch.replace('refs/heads/', '')
46
- : 'detached';
47
- const isMain = wt.isMainWorktree ? ' (main)' : '';
42
+ // Prepare worktree items and calculate layout
43
+ const items = prepareWorktreeItems(worktrees, sessions);
44
+ const columnPositions = calculateColumnPositions(items);
45
+ // Build menu items with proper alignment
46
+ const menuItems = items.map((item, index) => {
47
+ const label = assembleWorktreeLabel(item, columnPositions);
48
48
  // Only show numbers for first 10 worktrees (0-9)
49
49
  const numberPrefix = index < 10 ? `${index} ❯ ` : '❯ ';
50
50
  return {
51
- label: `${numberPrefix}${branchName}${isMain}${status}`,
52
- value: wt.path,
53
- worktree: wt,
51
+ label: numberPrefix + label,
52
+ value: item.worktree.path,
53
+ worktree: item.worktree,
54
54
  };
55
55
  });
56
56
  // Add menu options
@@ -79,7 +79,7 @@ const Menu = ({ sessionManager, onSelectWorktree }) => {
79
79
  value: 'exit',
80
80
  });
81
81
  setItems(menuItems);
82
- }, [worktrees, sessions]);
82
+ }, [worktrees, sessions, defaultBranch]);
83
83
  // Handle hotkeys
84
84
  useInput((input, _key) => {
85
85
  const keyPressed = input.toLowerCase();
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ interface PresetSelectorProps {
3
+ onSelect: (presetId: string) => void;
4
+ onCancel: () => void;
5
+ }
6
+ declare const PresetSelector: React.FC<PresetSelectorProps>;
7
+ export default PresetSelector;
@@ -0,0 +1,52 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import SelectInput from 'ink-select-input';
4
+ import { configurationManager } from '../services/configurationManager.js';
5
+ const PresetSelector = ({ onSelect, onCancel, }) => {
6
+ const presetsConfig = configurationManager.getCommandPresets();
7
+ const [presets] = useState(presetsConfig.presets);
8
+ const defaultPresetId = presetsConfig.defaultPresetId;
9
+ const selectItems = presets.map(preset => {
10
+ const isDefault = preset.id === defaultPresetId;
11
+ const args = preset.args?.join(' ') || '';
12
+ const fallback = preset.fallbackArgs?.join(' ') || '';
13
+ let label = preset.name;
14
+ if (isDefault)
15
+ label += ' (default)';
16
+ label += `\n Command: ${preset.command}`;
17
+ if (args)
18
+ label += `\n Args: ${args}`;
19
+ if (fallback)
20
+ label += `\n Fallback: ${fallback}`;
21
+ return {
22
+ label,
23
+ value: preset.id,
24
+ };
25
+ });
26
+ // Add cancel option
27
+ selectItems.push({ label: '← Cancel', value: 'cancel' });
28
+ const handleSelectItem = (item) => {
29
+ if (item.value === 'cancel') {
30
+ onCancel();
31
+ }
32
+ else {
33
+ onSelect(item.value);
34
+ }
35
+ };
36
+ // Find initial index based on default preset
37
+ const initialIndex = selectItems.findIndex(item => item.value === defaultPresetId);
38
+ useInput((input, key) => {
39
+ if (key.escape) {
40
+ onCancel();
41
+ }
42
+ });
43
+ return (React.createElement(Box, { flexDirection: "column" },
44
+ React.createElement(Box, { marginBottom: 1 },
45
+ React.createElement(Text, { bold: true, color: "green" }, "Select Command Preset")),
46
+ React.createElement(Box, { marginBottom: 1 },
47
+ React.createElement(Text, { dimColor: true }, "Choose a preset to start the session with")),
48
+ React.createElement(SelectInput, { items: selectItems, onSelect: handleSelectItem, initialIndex: initialIndex >= 0 ? initialIndex : 0 }),
49
+ React.createElement(Box, { marginTop: 1 },
50
+ React.createElement(Text, { dimColor: true }, "Press \u2191\u2193 to navigate, Enter to select, ESC to cancel"))));
51
+ };
52
+ export default PresetSelector;
@@ -0,0 +1,2 @@
1
+ import { Worktree } from '../types/index.js';
2
+ export declare function useGitStatus(worktrees: Worktree[], defaultBranch: string | null, updateInterval?: number): Worktree[];
@@ -0,0 +1,52 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { getGitStatusLimited } from '../utils/gitStatus.js';
3
+ export function useGitStatus(worktrees, defaultBranch, updateInterval = 5000) {
4
+ const [worktreesWithStatus, setWorktreesWithStatus] = useState(worktrees);
5
+ useEffect(() => {
6
+ if (!defaultBranch) {
7
+ return;
8
+ }
9
+ const timeouts = new Map();
10
+ const activeRequests = new Map();
11
+ let isCleanedUp = false;
12
+ const fetchStatus = async (worktree, abortController) => {
13
+ try {
14
+ const result = await getGitStatusLimited(worktree.path, abortController.signal);
15
+ if (result.data || result.error) {
16
+ setWorktreesWithStatus(prev => prev.map(wt => wt.path === worktree.path
17
+ ? { ...wt, gitStatus: result.data, gitStatusError: result.error }
18
+ : wt));
19
+ }
20
+ }
21
+ catch {
22
+ // Ignore errors - the fetch failed or was aborted
23
+ }
24
+ };
25
+ const scheduleUpdate = (worktree) => {
26
+ const abortController = new AbortController();
27
+ activeRequests.set(worktree.path, abortController);
28
+ fetchStatus(worktree, abortController).finally(() => {
29
+ const isActive = () => !isCleanedUp && !abortController.signal.aborted;
30
+ if (isActive()) {
31
+ const timeout = setTimeout(() => {
32
+ if (isActive()) {
33
+ scheduleUpdate(worktree);
34
+ }
35
+ }, updateInterval);
36
+ timeouts.set(worktree.path, timeout);
37
+ }
38
+ });
39
+ };
40
+ setWorktreesWithStatus(worktrees);
41
+ // Start fetching for each worktree
42
+ worktrees.forEach(worktree => {
43
+ scheduleUpdate(worktree);
44
+ });
45
+ return () => {
46
+ isCleanedUp = true;
47
+ timeouts.forEach(timeout => clearTimeout(timeout));
48
+ activeRequests.forEach(controller => controller.abort());
49
+ };
50
+ }, [worktrees, defaultBranch, updateInterval]);
51
+ return worktreesWithStatus;
52
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,186 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import React from 'react';
3
+ import { render, cleanup } from 'ink-testing-library';
4
+ import { Text } from 'ink';
5
+ import { useGitStatus } from './useGitStatus.js';
6
+ import { getGitStatusLimited } from '../utils/gitStatus.js';
7
+ // Mock the gitStatus module
8
+ vi.mock('../utils/gitStatus.js', () => ({
9
+ getGitStatusLimited: vi.fn(),
10
+ }));
11
+ describe('useGitStatus', () => {
12
+ const mockGetGitStatus = getGitStatusLimited;
13
+ const createWorktree = (path) => ({
14
+ path,
15
+ branch: 'main',
16
+ isMainWorktree: false,
17
+ hasSession: false,
18
+ });
19
+ const createGitStatus = (added = 1, deleted = 0) => ({
20
+ filesAdded: added,
21
+ filesDeleted: deleted,
22
+ aheadCount: 0,
23
+ behindCount: 0,
24
+ parentBranch: 'main',
25
+ });
26
+ beforeEach(() => {
27
+ vi.useFakeTimers();
28
+ mockGetGitStatus.mockClear();
29
+ });
30
+ afterEach(() => {
31
+ vi.useRealTimers();
32
+ cleanup();
33
+ });
34
+ // Main behavioral test
35
+ it('should fetch and update git status for worktrees', async () => {
36
+ const worktrees = [createWorktree('/path1'), createWorktree('/path2')];
37
+ const gitStatus1 = createGitStatus(5, 3);
38
+ const gitStatus2 = createGitStatus(2, 1);
39
+ let hookResult = [];
40
+ mockGetGitStatus.mockImplementation(async (path) => {
41
+ if (path === '/path1') {
42
+ return { success: true, data: gitStatus1 };
43
+ }
44
+ return { success: true, data: gitStatus2 };
45
+ });
46
+ const TestComponent = () => {
47
+ hookResult = useGitStatus(worktrees, 'main', 100);
48
+ return React.createElement(Text, null, 'test');
49
+ };
50
+ render(React.createElement(TestComponent));
51
+ // Should return worktrees immediately
52
+ expect(hookResult).toEqual(worktrees);
53
+ // Wait for status updates
54
+ await vi.waitFor(() => {
55
+ expect(hookResult[0]?.gitStatus).toBeDefined();
56
+ expect(hookResult[1]?.gitStatus).toBeDefined();
57
+ });
58
+ // Should have correct status for each worktree
59
+ expect(hookResult[0]?.gitStatus).toEqual(gitStatus1);
60
+ expect(hookResult[1]?.gitStatus).toEqual(gitStatus2);
61
+ });
62
+ it('should handle empty worktree array', () => {
63
+ let hookResult = [];
64
+ const TestComponent = () => {
65
+ hookResult = useGitStatus([], 'main');
66
+ return React.createElement(Text, null, 'test');
67
+ };
68
+ render(React.createElement(TestComponent));
69
+ expect(hookResult).toEqual([]);
70
+ expect(mockGetGitStatus).not.toHaveBeenCalled();
71
+ });
72
+ it('should not fetch when defaultBranch is null', async () => {
73
+ const worktrees = [createWorktree('/path1'), createWorktree('/path2')];
74
+ let hookResult = [];
75
+ const TestComponent = () => {
76
+ hookResult = useGitStatus(worktrees, null);
77
+ return React.createElement(Text, null, 'test');
78
+ };
79
+ render(React.createElement(TestComponent));
80
+ // Should return worktrees immediately without modification
81
+ expect(hookResult).toEqual(worktrees);
82
+ // Wait to ensure no fetches occur
83
+ await vi.advanceTimersByTimeAsync(1000);
84
+ expect(mockGetGitStatus).not.toHaveBeenCalled();
85
+ });
86
+ it('should continue polling after errors', async () => {
87
+ const worktrees = [createWorktree('/path1')];
88
+ mockGetGitStatus.mockResolvedValue({
89
+ success: false,
90
+ error: 'Git error',
91
+ });
92
+ const TestComponent = () => {
93
+ useGitStatus(worktrees, 'main', 100);
94
+ return React.createElement(Text, null, 'test');
95
+ };
96
+ render(React.createElement(TestComponent));
97
+ // Wait for initial fetch
98
+ await vi.waitFor(() => {
99
+ expect(mockGetGitStatus).toHaveBeenCalledTimes(1);
100
+ });
101
+ // Clear to track subsequent calls
102
+ mockGetGitStatus.mockClear();
103
+ // Advance time and verify polling continues despite errors
104
+ await vi.advanceTimersByTimeAsync(100);
105
+ expect(mockGetGitStatus).toHaveBeenCalledTimes(1);
106
+ await vi.advanceTimersByTimeAsync(100);
107
+ expect(mockGetGitStatus).toHaveBeenCalledTimes(2);
108
+ // All calls should have been made despite continuous errors
109
+ expect(mockGetGitStatus).toHaveBeenCalledWith('/path1', expect.any(AbortSignal));
110
+ });
111
+ it('should handle slow git operations that exceed update interval', async () => {
112
+ const worktrees = [createWorktree('/path1')];
113
+ let fetchCount = 0;
114
+ let resolveFetch = null;
115
+ mockGetGitStatus.mockImplementation(async () => {
116
+ fetchCount++;
117
+ // Create a promise that we can resolve manually
118
+ return new Promise(resolve => {
119
+ resolveFetch = resolve;
120
+ });
121
+ });
122
+ const TestComponent = () => {
123
+ useGitStatus(worktrees, 'main', 100);
124
+ return React.createElement(Text, null, 'test');
125
+ };
126
+ render(React.createElement(TestComponent));
127
+ // Wait for initial fetch to start
128
+ await vi.waitFor(() => {
129
+ expect(mockGetGitStatus).toHaveBeenCalledTimes(1);
130
+ });
131
+ // Advance time past the update interval while fetch is still pending
132
+ await vi.advanceTimersByTimeAsync(250);
133
+ // Should not have started a second fetch yet
134
+ expect(mockGetGitStatus).toHaveBeenCalledTimes(1);
135
+ // Complete the first fetch
136
+ resolveFetch({ success: true, data: createGitStatus(1, 0) });
137
+ // Wait for the promise to resolve
138
+ await vi.waitFor(() => {
139
+ expect(fetchCount).toBe(1);
140
+ });
141
+ // Now advance time by the update interval
142
+ await vi.advanceTimersByTimeAsync(100);
143
+ // Should have started the second fetch
144
+ await vi.waitFor(() => {
145
+ expect(mockGetGitStatus).toHaveBeenCalledTimes(2);
146
+ });
147
+ });
148
+ it('should properly cleanup resources when worktrees change', async () => {
149
+ let activeRequests = 0;
150
+ const abortedSignals = [];
151
+ mockGetGitStatus.mockImplementation(async (path, signal) => {
152
+ activeRequests++;
153
+ signal.addEventListener('abort', () => {
154
+ activeRequests--;
155
+ abortedSignals.push(signal);
156
+ });
157
+ // Simulate ongoing request
158
+ return new Promise(() => { });
159
+ });
160
+ const TestComponent = ({ worktrees }) => {
161
+ useGitStatus(worktrees, 'main', 100);
162
+ return React.createElement(Text, null, 'test');
163
+ };
164
+ // Start with 3 worktrees
165
+ const initialWorktrees = [
166
+ createWorktree('/path1'),
167
+ createWorktree('/path2'),
168
+ createWorktree('/path3'),
169
+ ];
170
+ const { rerender } = render(React.createElement(TestComponent, { worktrees: initialWorktrees }));
171
+ // Should have 3 active requests
172
+ await vi.waitFor(() => {
173
+ expect(activeRequests).toBe(3);
174
+ });
175
+ // Change to 2 different worktrees
176
+ const newWorktrees = [createWorktree('/path4'), createWorktree('/path5')];
177
+ rerender(React.createElement(TestComponent, { worktrees: newWorktrees }));
178
+ // Wait for cleanup and new requests
179
+ await vi.waitFor(() => {
180
+ expect(abortedSignals).toHaveLength(3);
181
+ expect(activeRequests).toBe(2);
182
+ });
183
+ // Verify all old signals were aborted
184
+ expect(abortedSignals.every(signal => signal.aborted)).toBe(true);
185
+ });
186
+ });
@@ -1,4 +1,4 @@
1
- import { ConfigurationData, StatusHookConfig, ShortcutConfig, WorktreeConfig, CommandConfig } from '../types/index.js';
1
+ import { ConfigurationData, StatusHookConfig, ShortcutConfig, WorktreeConfig, CommandConfig, CommandPreset, CommandPresetsConfig } from '../types/index.js';
2
2
  export declare class ConfigurationManager {
3
3
  private configPath;
4
4
  private legacyShortcutsPath;
@@ -17,5 +17,15 @@ export declare class ConfigurationManager {
17
17
  setWorktreeConfig(worktreeConfig: WorktreeConfig): void;
18
18
  getCommandConfig(): CommandConfig;
19
19
  setCommandConfig(commandConfig: CommandConfig): void;
20
+ private migrateLegacyCommandToPresets;
21
+ getCommandPresets(): CommandPresetsConfig;
22
+ setCommandPresets(presets: CommandPresetsConfig): void;
23
+ getDefaultPreset(): CommandPreset;
24
+ getPresetById(id: string): CommandPreset | undefined;
25
+ addPreset(preset: CommandPreset): void;
26
+ deletePreset(id: string): void;
27
+ setDefaultPreset(id: string): void;
28
+ getSelectPresetOnStart(): boolean;
29
+ setSelectPresetOnStart(enabled: boolean): void;
20
30
  }
21
31
  export declare const configurationManager: ConfigurationManager;
@@ -73,6 +73,8 @@ export class ConfigurationManager {
73
73
  command: 'claude',
74
74
  };
75
75
  }
76
+ // Migrate legacy command config to presets if needed
77
+ this.migrateLegacyCommandToPresets();
76
78
  }
77
79
  migrateLegacyShortcuts() {
78
80
  if (existsSync(this.legacyShortcutsPath)) {
@@ -131,13 +133,119 @@ export class ConfigurationManager {
131
133
  this.saveConfig();
132
134
  }
133
135
  getCommandConfig() {
134
- return (this.config.command || {
135
- command: 'claude',
136
- });
136
+ // For backward compatibility, return the default preset as CommandConfig
137
+ const defaultPreset = this.getDefaultPreset();
138
+ return {
139
+ command: defaultPreset.command,
140
+ args: defaultPreset.args,
141
+ fallbackArgs: defaultPreset.fallbackArgs,
142
+ };
137
143
  }
138
144
  setCommandConfig(commandConfig) {
139
145
  this.config.command = commandConfig;
146
+ // Also update the default preset for backward compatibility
147
+ if (this.config.commandPresets) {
148
+ const defaultPreset = this.config.commandPresets.presets.find(p => p.id === this.config.commandPresets.defaultPresetId);
149
+ if (defaultPreset) {
150
+ defaultPreset.command = commandConfig.command;
151
+ defaultPreset.args = commandConfig.args;
152
+ defaultPreset.fallbackArgs = commandConfig.fallbackArgs;
153
+ }
154
+ }
155
+ this.saveConfig();
156
+ }
157
+ migrateLegacyCommandToPresets() {
158
+ // Only migrate if we have legacy command config but no presets
159
+ if (this.config.command && !this.config.commandPresets) {
160
+ const defaultPreset = {
161
+ id: '1',
162
+ name: 'Main',
163
+ command: this.config.command.command,
164
+ args: this.config.command.args,
165
+ fallbackArgs: this.config.command.fallbackArgs,
166
+ };
167
+ this.config.commandPresets = {
168
+ presets: [defaultPreset],
169
+ defaultPresetId: '1',
170
+ };
171
+ this.saveConfig();
172
+ }
173
+ // Ensure default presets if none exist
174
+ if (!this.config.commandPresets) {
175
+ this.config.commandPresets = {
176
+ presets: [
177
+ {
178
+ id: '1',
179
+ name: 'Main',
180
+ command: 'claude',
181
+ },
182
+ ],
183
+ defaultPresetId: '1',
184
+ };
185
+ }
186
+ }
187
+ getCommandPresets() {
188
+ if (!this.config.commandPresets) {
189
+ this.migrateLegacyCommandToPresets();
190
+ }
191
+ return this.config.commandPresets;
192
+ }
193
+ setCommandPresets(presets) {
194
+ this.config.commandPresets = presets;
140
195
  this.saveConfig();
141
196
  }
197
+ getDefaultPreset() {
198
+ const presets = this.getCommandPresets();
199
+ const defaultPreset = presets.presets.find(p => p.id === presets.defaultPresetId);
200
+ // If default preset not found, return the first one
201
+ return defaultPreset || presets.presets[0];
202
+ }
203
+ getPresetById(id) {
204
+ const presets = this.getCommandPresets();
205
+ return presets.presets.find(p => p.id === id);
206
+ }
207
+ addPreset(preset) {
208
+ const presets = this.getCommandPresets();
209
+ // Replace if exists, otherwise add
210
+ const existingIndex = presets.presets.findIndex(p => p.id === preset.id);
211
+ if (existingIndex >= 0) {
212
+ presets.presets[existingIndex] = preset;
213
+ }
214
+ else {
215
+ presets.presets.push(preset);
216
+ }
217
+ this.setCommandPresets(presets);
218
+ }
219
+ deletePreset(id) {
220
+ const presets = this.getCommandPresets();
221
+ // Don't delete if it's the last preset
222
+ if (presets.presets.length <= 1) {
223
+ return;
224
+ }
225
+ // Remove the preset
226
+ presets.presets = presets.presets.filter(p => p.id !== id);
227
+ // Update default if needed
228
+ if (presets.defaultPresetId === id && presets.presets.length > 0) {
229
+ presets.defaultPresetId = presets.presets[0].id;
230
+ }
231
+ this.setCommandPresets(presets);
232
+ }
233
+ setDefaultPreset(id) {
234
+ const presets = this.getCommandPresets();
235
+ // Only update if preset exists
236
+ if (presets.presets.some(p => p.id === id)) {
237
+ presets.defaultPresetId = id;
238
+ this.setCommandPresets(presets);
239
+ }
240
+ }
241
+ getSelectPresetOnStart() {
242
+ const presets = this.getCommandPresets();
243
+ return presets.selectPresetOnStart ?? false;
244
+ }
245
+ setSelectPresetOnStart(enabled) {
246
+ const presets = this.getCommandPresets();
247
+ presets.selectPresetOnStart = enabled;
248
+ this.setCommandPresets(presets);
249
+ }
142
250
  }
143
251
  export const configurationManager = new ConfigurationManager();
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
+ import { ConfigurationManager } from './configurationManager.js';
4
+ // Mock fs module
5
+ vi.mock('fs', () => ({
6
+ existsSync: vi.fn(),
7
+ mkdirSync: vi.fn(),
8
+ readFileSync: vi.fn(),
9
+ writeFileSync: vi.fn(),
10
+ }));
11
+ // Mock os module
12
+ vi.mock('os', () => ({
13
+ homedir: vi.fn(() => '/home/test'),
14
+ }));
15
+ describe('ConfigurationManager - selectPresetOnStart', () => {
16
+ let configManager;
17
+ let mockConfigData;
18
+ beforeEach(() => {
19
+ // Reset all mocks
20
+ vi.clearAllMocks();
21
+ // Default mock config data
22
+ mockConfigData = {
23
+ shortcuts: {
24
+ returnToMenu: { ctrl: true, key: 'e' },
25
+ cancel: { key: 'escape' },
26
+ },
27
+ commandPresets: {
28
+ presets: [
29
+ {
30
+ id: '1',
31
+ name: 'Main',
32
+ command: 'claude',
33
+ },
34
+ {
35
+ id: '2',
36
+ name: 'Development',
37
+ command: 'claude',
38
+ args: ['--resume'],
39
+ },
40
+ ],
41
+ defaultPresetId: '1',
42
+ },
43
+ };
44
+ // Mock file system operations
45
+ existsSync.mockImplementation((path) => {
46
+ return path.includes('config.json');
47
+ });
48
+ readFileSync.mockImplementation(() => {
49
+ return JSON.stringify(mockConfigData);
50
+ });
51
+ mkdirSync.mockImplementation(() => { });
52
+ writeFileSync.mockImplementation(() => { });
53
+ // Create new instance for each test
54
+ configManager = new ConfigurationManager();
55
+ });
56
+ afterEach(() => {
57
+ vi.resetAllMocks();
58
+ });
59
+ describe('getSelectPresetOnStart', () => {
60
+ it('should return false by default', () => {
61
+ const result = configManager.getSelectPresetOnStart();
62
+ expect(result).toBe(false);
63
+ });
64
+ it('should return true when configured', () => {
65
+ mockConfigData.commandPresets.selectPresetOnStart = true;
66
+ configManager = new ConfigurationManager();
67
+ const result = configManager.getSelectPresetOnStart();
68
+ expect(result).toBe(true);
69
+ });
70
+ it('should return false when explicitly set to false', () => {
71
+ mockConfigData.commandPresets.selectPresetOnStart = false;
72
+ configManager = new ConfigurationManager();
73
+ const result = configManager.getSelectPresetOnStart();
74
+ expect(result).toBe(false);
75
+ });
76
+ });
77
+ describe('setSelectPresetOnStart', () => {
78
+ it('should set selectPresetOnStart to true', () => {
79
+ configManager.setSelectPresetOnStart(true);
80
+ const result = configManager.getSelectPresetOnStart();
81
+ expect(result).toBe(true);
82
+ // Verify that config was saved
83
+ expect(writeFileSync).toHaveBeenCalledWith(expect.stringContaining('config.json'), expect.stringContaining('"selectPresetOnStart": true'));
84
+ });
85
+ it('should set selectPresetOnStart to false', () => {
86
+ // First set to true
87
+ configManager.setSelectPresetOnStart(true);
88
+ // Then set to false
89
+ configManager.setSelectPresetOnStart(false);
90
+ const result = configManager.getSelectPresetOnStart();
91
+ expect(result).toBe(false);
92
+ // Verify that config was saved
93
+ expect(writeFileSync).toHaveBeenLastCalledWith(expect.stringContaining('config.json'), expect.stringContaining('"selectPresetOnStart": false'));
94
+ });
95
+ it('should preserve other preset configuration when setting selectPresetOnStart', () => {
96
+ configManager.setSelectPresetOnStart(true);
97
+ const presets = configManager.getCommandPresets();
98
+ expect(presets.presets).toHaveLength(2);
99
+ expect(presets.defaultPresetId).toBe('1');
100
+ expect(presets.selectPresetOnStart).toBe(true);
101
+ });
102
+ });
103
+ });
@@ -0,0 +1 @@
1
+ export {};