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.
- package/dist/cli.js +3 -0
- package/dist/components/App.js +35 -1
- package/dist/components/ConfigureCommand.js +367 -121
- package/dist/components/Menu.js +18 -18
- package/dist/components/PresetSelector.d.ts +7 -0
- package/dist/components/PresetSelector.js +52 -0
- package/dist/hooks/useGitStatus.d.ts +2 -0
- package/dist/hooks/useGitStatus.js +52 -0
- package/dist/hooks/useGitStatus.test.d.ts +1 -0
- package/dist/hooks/useGitStatus.test.js +186 -0
- package/dist/services/configurationManager.d.ts +11 -1
- package/dist/services/configurationManager.js +111 -3
- package/dist/services/configurationManager.selectPresetOnStart.test.d.ts +1 -0
- package/dist/services/configurationManager.selectPresetOnStart.test.js +103 -0
- package/dist/services/configurationManager.test.d.ts +1 -0
- package/dist/services/configurationManager.test.js +313 -0
- package/dist/services/sessionManager.d.ts +1 -0
- package/dist/services/sessionManager.js +69 -0
- package/dist/services/sessionManager.test.js +103 -0
- package/dist/services/worktreeConfigManager.d.ts +10 -0
- package/dist/services/worktreeConfigManager.js +27 -0
- package/dist/services/worktreeService.js +8 -0
- package/dist/services/worktreeService.test.js +8 -0
- package/dist/types/index.d.ts +16 -0
- package/dist/utils/concurrencyLimit.d.ts +4 -0
- package/dist/utils/concurrencyLimit.js +30 -0
- package/dist/utils/concurrencyLimit.test.d.ts +1 -0
- package/dist/utils/concurrencyLimit.test.js +63 -0
- package/dist/utils/gitStatus.d.ts +19 -0
- package/dist/utils/gitStatus.js +146 -0
- package/dist/utils/gitStatus.test.d.ts +1 -0
- package/dist/utils/gitStatus.test.js +141 -0
- package/dist/utils/worktreeConfig.d.ts +3 -0
- package/dist/utils/worktreeConfig.js +43 -0
- package/dist/utils/worktreeUtils.d.ts +37 -0
- package/dist/utils/worktreeUtils.js +114 -0
- package/dist/utils/worktreeUtils.test.js +105 -1
- package/package.json +1 -1
|
@@ -0,0 +1,313 @@
|
|
|
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 - Command Presets', () => {
|
|
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
|
+
command: {
|
|
28
|
+
command: 'claude',
|
|
29
|
+
args: ['--existing'],
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
// Mock file system operations
|
|
33
|
+
existsSync.mockImplementation((path) => {
|
|
34
|
+
return path.includes('config.json');
|
|
35
|
+
});
|
|
36
|
+
readFileSync.mockImplementation(() => {
|
|
37
|
+
return JSON.stringify(mockConfigData);
|
|
38
|
+
});
|
|
39
|
+
mkdirSync.mockImplementation(() => { });
|
|
40
|
+
writeFileSync.mockImplementation(() => { });
|
|
41
|
+
// Create new instance for each test
|
|
42
|
+
configManager = new ConfigurationManager();
|
|
43
|
+
});
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
vi.resetAllMocks();
|
|
46
|
+
});
|
|
47
|
+
describe('getCommandPresets', () => {
|
|
48
|
+
it('should return default presets when no presets are configured', () => {
|
|
49
|
+
// Remove command config for this test
|
|
50
|
+
delete mockConfigData.command;
|
|
51
|
+
configManager = new ConfigurationManager();
|
|
52
|
+
const presets = configManager.getCommandPresets();
|
|
53
|
+
expect(presets).toBeDefined();
|
|
54
|
+
expect(presets.presets).toHaveLength(1);
|
|
55
|
+
expect(presets.presets[0]).toEqual({
|
|
56
|
+
id: '1',
|
|
57
|
+
name: 'Main',
|
|
58
|
+
command: 'claude',
|
|
59
|
+
});
|
|
60
|
+
expect(presets.defaultPresetId).toBe('1');
|
|
61
|
+
});
|
|
62
|
+
it('should return configured presets', () => {
|
|
63
|
+
mockConfigData.commandPresets = {
|
|
64
|
+
presets: [
|
|
65
|
+
{ id: '1', name: 'Main', command: 'claude' },
|
|
66
|
+
{ id: '2', name: 'Development', command: 'claude', args: ['--resume'] },
|
|
67
|
+
],
|
|
68
|
+
defaultPresetId: '2',
|
|
69
|
+
};
|
|
70
|
+
configManager = new ConfigurationManager();
|
|
71
|
+
const presets = configManager.getCommandPresets();
|
|
72
|
+
expect(presets.presets).toHaveLength(2);
|
|
73
|
+
expect(presets.defaultPresetId).toBe('2');
|
|
74
|
+
});
|
|
75
|
+
it('should migrate legacy command config to presets on first access', () => {
|
|
76
|
+
// Config has legacy command but no presets
|
|
77
|
+
mockConfigData.command = {
|
|
78
|
+
command: 'claude',
|
|
79
|
+
args: ['--resume'],
|
|
80
|
+
fallbackArgs: ['--no-mcp'],
|
|
81
|
+
};
|
|
82
|
+
delete mockConfigData.commandPresets;
|
|
83
|
+
configManager = new ConfigurationManager();
|
|
84
|
+
const presets = configManager.getCommandPresets();
|
|
85
|
+
expect(presets.presets).toHaveLength(1);
|
|
86
|
+
expect(presets.presets[0]).toEqual({
|
|
87
|
+
id: '1',
|
|
88
|
+
name: 'Main',
|
|
89
|
+
command: 'claude',
|
|
90
|
+
args: ['--resume'],
|
|
91
|
+
fallbackArgs: ['--no-mcp'],
|
|
92
|
+
});
|
|
93
|
+
expect(presets.defaultPresetId).toBe('1');
|
|
94
|
+
// Verify that writeFileSync was called to save the migration
|
|
95
|
+
expect(writeFileSync).toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('setCommandPresets', () => {
|
|
99
|
+
it('should save new presets configuration', () => {
|
|
100
|
+
const newPresets = {
|
|
101
|
+
presets: [
|
|
102
|
+
{ id: '1', name: 'Main', command: 'claude' },
|
|
103
|
+
{ id: '2', name: 'Custom', command: 'claude', args: ['--custom'] },
|
|
104
|
+
],
|
|
105
|
+
defaultPresetId: '2',
|
|
106
|
+
};
|
|
107
|
+
configManager.setCommandPresets(newPresets);
|
|
108
|
+
expect(writeFileSync).toHaveBeenCalledWith(expect.stringContaining('config.json'), expect.stringContaining('commandPresets'));
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('getDefaultPreset', () => {
|
|
112
|
+
it('should return the default preset', () => {
|
|
113
|
+
mockConfigData.commandPresets = {
|
|
114
|
+
presets: [
|
|
115
|
+
{ id: '1', name: 'Main', command: 'claude' },
|
|
116
|
+
{ id: '2', name: 'Custom', command: 'claude', args: ['--custom'] },
|
|
117
|
+
],
|
|
118
|
+
defaultPresetId: '2',
|
|
119
|
+
};
|
|
120
|
+
configManager = new ConfigurationManager();
|
|
121
|
+
const defaultPreset = configManager.getDefaultPreset();
|
|
122
|
+
expect(defaultPreset).toEqual({
|
|
123
|
+
id: '2',
|
|
124
|
+
name: 'Custom',
|
|
125
|
+
command: 'claude',
|
|
126
|
+
args: ['--custom'],
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
it('should return first preset if defaultPresetId is invalid', () => {
|
|
130
|
+
mockConfigData.commandPresets = {
|
|
131
|
+
presets: [
|
|
132
|
+
{ id: '1', name: 'Main', command: 'claude' },
|
|
133
|
+
{ id: '2', name: 'Custom', command: 'claude', args: ['--custom'] },
|
|
134
|
+
],
|
|
135
|
+
defaultPresetId: 'invalid',
|
|
136
|
+
};
|
|
137
|
+
configManager = new ConfigurationManager();
|
|
138
|
+
const defaultPreset = configManager.getDefaultPreset();
|
|
139
|
+
expect(defaultPreset).toEqual({
|
|
140
|
+
id: '1',
|
|
141
|
+
name: 'Main',
|
|
142
|
+
command: 'claude',
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
describe('getPresetById', () => {
|
|
147
|
+
it('should return preset by id', () => {
|
|
148
|
+
mockConfigData.commandPresets = {
|
|
149
|
+
presets: [
|
|
150
|
+
{ id: '1', name: 'Main', command: 'claude' },
|
|
151
|
+
{ id: '2', name: 'Custom', command: 'claude', args: ['--custom'] },
|
|
152
|
+
],
|
|
153
|
+
defaultPresetId: '1',
|
|
154
|
+
};
|
|
155
|
+
configManager = new ConfigurationManager();
|
|
156
|
+
const preset = configManager.getPresetById('2');
|
|
157
|
+
expect(preset).toEqual({
|
|
158
|
+
id: '2',
|
|
159
|
+
name: 'Custom',
|
|
160
|
+
command: 'claude',
|
|
161
|
+
args: ['--custom'],
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
it('should return undefined for non-existent id', () => {
|
|
165
|
+
mockConfigData.commandPresets = {
|
|
166
|
+
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
167
|
+
defaultPresetId: '1',
|
|
168
|
+
};
|
|
169
|
+
configManager = new ConfigurationManager();
|
|
170
|
+
const preset = configManager.getPresetById('999');
|
|
171
|
+
expect(preset).toBeUndefined();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
describe('addPreset', () => {
|
|
175
|
+
it('should add a new preset', () => {
|
|
176
|
+
mockConfigData.commandPresets = {
|
|
177
|
+
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
178
|
+
defaultPresetId: '1',
|
|
179
|
+
};
|
|
180
|
+
configManager = new ConfigurationManager();
|
|
181
|
+
const newPreset = {
|
|
182
|
+
id: '2',
|
|
183
|
+
name: 'New Preset',
|
|
184
|
+
command: 'claude',
|
|
185
|
+
args: ['--new'],
|
|
186
|
+
};
|
|
187
|
+
configManager.addPreset(newPreset);
|
|
188
|
+
const presets = configManager.getCommandPresets();
|
|
189
|
+
expect(presets.presets).toHaveLength(2);
|
|
190
|
+
expect(presets.presets[1]).toEqual(newPreset);
|
|
191
|
+
});
|
|
192
|
+
it('should replace preset with same id', () => {
|
|
193
|
+
mockConfigData.commandPresets = {
|
|
194
|
+
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
195
|
+
defaultPresetId: '1',
|
|
196
|
+
};
|
|
197
|
+
configManager = new ConfigurationManager();
|
|
198
|
+
const updatedPreset = {
|
|
199
|
+
id: '1',
|
|
200
|
+
name: 'Updated Default',
|
|
201
|
+
command: 'claude',
|
|
202
|
+
args: ['--updated'],
|
|
203
|
+
};
|
|
204
|
+
configManager.addPreset(updatedPreset);
|
|
205
|
+
const presets = configManager.getCommandPresets();
|
|
206
|
+
expect(presets.presets).toHaveLength(1);
|
|
207
|
+
expect(presets.presets[0]).toEqual(updatedPreset);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
describe('deletePreset', () => {
|
|
211
|
+
it('should delete preset by id', () => {
|
|
212
|
+
mockConfigData.commandPresets = {
|
|
213
|
+
presets: [
|
|
214
|
+
{ id: '1', name: 'Main', command: 'claude' },
|
|
215
|
+
{ id: '2', name: 'Custom', command: 'claude', args: ['--custom'] },
|
|
216
|
+
],
|
|
217
|
+
defaultPresetId: '1',
|
|
218
|
+
};
|
|
219
|
+
configManager = new ConfigurationManager();
|
|
220
|
+
configManager.deletePreset('2');
|
|
221
|
+
const presets = configManager.getCommandPresets();
|
|
222
|
+
expect(presets.presets).toHaveLength(1);
|
|
223
|
+
expect(presets.presets[0].id).toBe('1');
|
|
224
|
+
});
|
|
225
|
+
it('should not delete the last preset', () => {
|
|
226
|
+
mockConfigData.commandPresets = {
|
|
227
|
+
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
228
|
+
defaultPresetId: '1',
|
|
229
|
+
};
|
|
230
|
+
configManager = new ConfigurationManager();
|
|
231
|
+
configManager.deletePreset('1');
|
|
232
|
+
const presets = configManager.getCommandPresets();
|
|
233
|
+
expect(presets.presets).toHaveLength(1);
|
|
234
|
+
});
|
|
235
|
+
it('should update defaultPresetId if default preset is deleted', () => {
|
|
236
|
+
mockConfigData.commandPresets = {
|
|
237
|
+
presets: [
|
|
238
|
+
{ id: '1', name: 'Main', command: 'claude' },
|
|
239
|
+
{ id: '2', name: 'Custom', command: 'claude', args: ['--custom'] },
|
|
240
|
+
],
|
|
241
|
+
defaultPresetId: '2',
|
|
242
|
+
};
|
|
243
|
+
configManager = new ConfigurationManager();
|
|
244
|
+
configManager.deletePreset('2');
|
|
245
|
+
const presets = configManager.getCommandPresets();
|
|
246
|
+
expect(presets.defaultPresetId).toBe('1');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
describe('setDefaultPreset', () => {
|
|
250
|
+
it('should update default preset id', () => {
|
|
251
|
+
mockConfigData.commandPresets = {
|
|
252
|
+
presets: [
|
|
253
|
+
{ id: '1', name: 'Main', command: 'claude' },
|
|
254
|
+
{ id: '2', name: 'Custom', command: 'claude', args: ['--custom'] },
|
|
255
|
+
],
|
|
256
|
+
defaultPresetId: '1',
|
|
257
|
+
};
|
|
258
|
+
configManager = new ConfigurationManager();
|
|
259
|
+
configManager.setDefaultPreset('2');
|
|
260
|
+
const presets = configManager.getCommandPresets();
|
|
261
|
+
expect(presets.defaultPresetId).toBe('2');
|
|
262
|
+
});
|
|
263
|
+
it('should not update if preset id does not exist', () => {
|
|
264
|
+
mockConfigData.commandPresets = {
|
|
265
|
+
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
266
|
+
defaultPresetId: '1',
|
|
267
|
+
};
|
|
268
|
+
configManager = new ConfigurationManager();
|
|
269
|
+
configManager.setDefaultPreset('999');
|
|
270
|
+
const presets = configManager.getCommandPresets();
|
|
271
|
+
expect(presets.defaultPresetId).toBe('1');
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
describe('backward compatibility', () => {
|
|
275
|
+
it('should maintain getCommandConfig for backward compatibility', () => {
|
|
276
|
+
mockConfigData.commandPresets = {
|
|
277
|
+
presets: [
|
|
278
|
+
{ id: '1', name: 'Main', command: 'claude', args: ['--resume'] },
|
|
279
|
+
{ id: '2', name: 'Custom', command: 'claude', args: ['--custom'] },
|
|
280
|
+
],
|
|
281
|
+
defaultPresetId: '1',
|
|
282
|
+
};
|
|
283
|
+
configManager = new ConfigurationManager();
|
|
284
|
+
const commandConfig = configManager.getCommandConfig();
|
|
285
|
+
// Should return the default preset as CommandConfig
|
|
286
|
+
expect(commandConfig).toEqual({
|
|
287
|
+
command: 'claude',
|
|
288
|
+
args: ['--resume'],
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
it('should update default preset when setCommandConfig is called', () => {
|
|
292
|
+
mockConfigData.commandPresets = {
|
|
293
|
+
presets: [{ id: '1', name: 'Main', command: 'claude' }],
|
|
294
|
+
defaultPresetId: '1',
|
|
295
|
+
};
|
|
296
|
+
configManager = new ConfigurationManager();
|
|
297
|
+
const newConfig = {
|
|
298
|
+
command: 'claude',
|
|
299
|
+
args: ['--new-args'],
|
|
300
|
+
fallbackArgs: ['--new-fallback'],
|
|
301
|
+
};
|
|
302
|
+
configManager.setCommandConfig(newConfig);
|
|
303
|
+
const presets = configManager.getCommandPresets();
|
|
304
|
+
expect(presets.presets[0]).toEqual({
|
|
305
|
+
id: '1',
|
|
306
|
+
name: 'Main',
|
|
307
|
+
command: 'claude',
|
|
308
|
+
args: ['--new-args'],
|
|
309
|
+
fallbackArgs: ['--new-fallback'],
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
});
|
|
@@ -10,6 +10,7 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
|
|
|
10
10
|
detectTerminalState(terminal: InstanceType<typeof Terminal>): SessionState;
|
|
11
11
|
constructor();
|
|
12
12
|
createSession(worktreePath: string): Promise<Session>;
|
|
13
|
+
createSessionWithPreset(worktreePath: string, presetId?: string): Promise<Session>;
|
|
13
14
|
private setupDataHandler;
|
|
14
15
|
private setupExitHandler;
|
|
15
16
|
private setupBackgroundHandler;
|
|
@@ -108,6 +108,75 @@ export class SessionManager extends EventEmitter {
|
|
|
108
108
|
this.emit('sessionCreated', session);
|
|
109
109
|
return session;
|
|
110
110
|
}
|
|
111
|
+
async createSessionWithPreset(worktreePath, presetId) {
|
|
112
|
+
// Check if session already exists
|
|
113
|
+
const existing = this.sessions.get(worktreePath);
|
|
114
|
+
if (existing) {
|
|
115
|
+
return existing;
|
|
116
|
+
}
|
|
117
|
+
const id = `session-${Date.now()}-${Math.random()
|
|
118
|
+
.toString(36)
|
|
119
|
+
.substr(2, 9)}`;
|
|
120
|
+
// Get preset configuration
|
|
121
|
+
let preset = presetId ? configurationManager.getPresetById(presetId) : null;
|
|
122
|
+
if (!preset) {
|
|
123
|
+
preset = configurationManager.getDefaultPreset();
|
|
124
|
+
}
|
|
125
|
+
const command = preset.command;
|
|
126
|
+
const args = preset.args || [];
|
|
127
|
+
const commandConfig = {
|
|
128
|
+
command: preset.command,
|
|
129
|
+
args: preset.args,
|
|
130
|
+
fallbackArgs: preset.fallbackArgs,
|
|
131
|
+
};
|
|
132
|
+
// Try to spawn the process
|
|
133
|
+
let ptyProcess;
|
|
134
|
+
let isPrimaryCommand = true;
|
|
135
|
+
try {
|
|
136
|
+
ptyProcess = await this.spawn(command, args, worktreePath);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
// If primary command fails and we have fallback args, try them
|
|
140
|
+
if (preset.fallbackArgs) {
|
|
141
|
+
try {
|
|
142
|
+
ptyProcess = await this.spawn(command, preset.fallbackArgs, worktreePath);
|
|
143
|
+
isPrimaryCommand = false;
|
|
144
|
+
}
|
|
145
|
+
catch (_fallbackError) {
|
|
146
|
+
// Both attempts failed, throw the original error
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// No fallback args, throw the error
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Create virtual terminal for state detection
|
|
156
|
+
const terminal = new Terminal({
|
|
157
|
+
cols: process.stdout.columns || 80,
|
|
158
|
+
rows: process.stdout.rows || 24,
|
|
159
|
+
allowProposedApi: true,
|
|
160
|
+
});
|
|
161
|
+
const session = {
|
|
162
|
+
id,
|
|
163
|
+
worktreePath,
|
|
164
|
+
process: ptyProcess,
|
|
165
|
+
state: 'busy', // Session starts as busy when created
|
|
166
|
+
output: [],
|
|
167
|
+
outputHistory: [],
|
|
168
|
+
lastActivity: new Date(),
|
|
169
|
+
isActive: false,
|
|
170
|
+
terminal,
|
|
171
|
+
isPrimaryCommand,
|
|
172
|
+
commandConfig,
|
|
173
|
+
};
|
|
174
|
+
// Set up persistent background data handler for state detection
|
|
175
|
+
this.setupBackgroundHandler(session);
|
|
176
|
+
this.sessions.set(worktreePath, session);
|
|
177
|
+
this.emit('sessionCreated', session);
|
|
178
|
+
return session;
|
|
179
|
+
}
|
|
111
180
|
setupDataHandler(session) {
|
|
112
181
|
// This handler always runs for all data
|
|
113
182
|
session.process.onData((data) => {
|
|
@@ -10,6 +10,8 @@ vi.mock('./configurationManager.js', () => ({
|
|
|
10
10
|
configurationManager: {
|
|
11
11
|
getCommandConfig: vi.fn(),
|
|
12
12
|
getStatusHooks: vi.fn(() => ({})),
|
|
13
|
+
getDefaultPreset: vi.fn(),
|
|
14
|
+
getPresetById: vi.fn(),
|
|
13
15
|
},
|
|
14
16
|
}));
|
|
15
17
|
// Mock Terminal
|
|
@@ -257,4 +259,105 @@ describe('SessionManager', () => {
|
|
|
257
259
|
expect(sessionManager.getSession('/test/worktree')).toBeUndefined();
|
|
258
260
|
});
|
|
259
261
|
});
|
|
262
|
+
describe('createSession with presets', () => {
|
|
263
|
+
it('should use default preset when no preset ID specified', async () => {
|
|
264
|
+
// Setup mock preset
|
|
265
|
+
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
266
|
+
id: '1',
|
|
267
|
+
name: 'Main',
|
|
268
|
+
command: 'claude',
|
|
269
|
+
args: ['--preset-arg'],
|
|
270
|
+
});
|
|
271
|
+
// Setup spawn mock
|
|
272
|
+
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
273
|
+
// Create session with preset
|
|
274
|
+
await sessionManager.createSessionWithPreset('/test/worktree');
|
|
275
|
+
// Verify spawn was called with preset config
|
|
276
|
+
expect(spawn).toHaveBeenCalledWith('claude', ['--preset-arg'], {
|
|
277
|
+
name: 'xterm-color',
|
|
278
|
+
cols: expect.any(Number),
|
|
279
|
+
rows: expect.any(Number),
|
|
280
|
+
cwd: '/test/worktree',
|
|
281
|
+
env: process.env,
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
it('should use specific preset when ID provided', async () => {
|
|
285
|
+
// Setup mock preset
|
|
286
|
+
vi.mocked(configurationManager.getPresetById).mockReturnValue({
|
|
287
|
+
id: '2',
|
|
288
|
+
name: 'Development',
|
|
289
|
+
command: 'claude',
|
|
290
|
+
args: ['--resume', '--dev'],
|
|
291
|
+
fallbackArgs: ['--no-mcp'],
|
|
292
|
+
});
|
|
293
|
+
// Setup spawn mock
|
|
294
|
+
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
295
|
+
// Create session with specific preset
|
|
296
|
+
await sessionManager.createSessionWithPreset('/test/worktree', '2');
|
|
297
|
+
// Verify getPresetById was called with correct ID
|
|
298
|
+
expect(configurationManager.getPresetById).toHaveBeenCalledWith('2');
|
|
299
|
+
// Verify spawn was called with preset config
|
|
300
|
+
expect(spawn).toHaveBeenCalledWith('claude', ['--resume', '--dev'], {
|
|
301
|
+
name: 'xterm-color',
|
|
302
|
+
cols: expect.any(Number),
|
|
303
|
+
rows: expect.any(Number),
|
|
304
|
+
cwd: '/test/worktree',
|
|
305
|
+
env: process.env,
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
it('should fall back to default preset if specified preset not found', async () => {
|
|
309
|
+
// Setup mocks
|
|
310
|
+
vi.mocked(configurationManager.getPresetById).mockReturnValue(undefined);
|
|
311
|
+
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
312
|
+
id: '1',
|
|
313
|
+
name: 'Main',
|
|
314
|
+
command: 'claude',
|
|
315
|
+
});
|
|
316
|
+
// Setup spawn mock
|
|
317
|
+
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
318
|
+
// Create session with non-existent preset
|
|
319
|
+
await sessionManager.createSessionWithPreset('/test/worktree', 'invalid');
|
|
320
|
+
// Verify fallback to default preset
|
|
321
|
+
expect(configurationManager.getDefaultPreset).toHaveBeenCalled();
|
|
322
|
+
expect(spawn).toHaveBeenCalledWith('claude', [], expect.any(Object));
|
|
323
|
+
});
|
|
324
|
+
it('should try fallback args with preset if main command fails', async () => {
|
|
325
|
+
// Setup mock preset with fallback
|
|
326
|
+
vi.mocked(configurationManager.getDefaultPreset).mockReturnValue({
|
|
327
|
+
id: '1',
|
|
328
|
+
name: 'Main',
|
|
329
|
+
command: 'claude',
|
|
330
|
+
args: ['--bad-flag'],
|
|
331
|
+
fallbackArgs: ['--good-flag'],
|
|
332
|
+
});
|
|
333
|
+
// Mock spawn to fail first, succeed second
|
|
334
|
+
let callCount = 0;
|
|
335
|
+
vi.mocked(spawn).mockImplementation(() => {
|
|
336
|
+
callCount++;
|
|
337
|
+
if (callCount === 1) {
|
|
338
|
+
throw new Error('Command failed');
|
|
339
|
+
}
|
|
340
|
+
return mockPty;
|
|
341
|
+
});
|
|
342
|
+
// Create session
|
|
343
|
+
await sessionManager.createSessionWithPreset('/test/worktree');
|
|
344
|
+
// Verify both attempts were made
|
|
345
|
+
expect(spawn).toHaveBeenCalledTimes(2);
|
|
346
|
+
expect(spawn).toHaveBeenNthCalledWith(1, 'claude', ['--bad-flag'], expect.any(Object));
|
|
347
|
+
expect(spawn).toHaveBeenNthCalledWith(2, 'claude', ['--good-flag'], expect.any(Object));
|
|
348
|
+
});
|
|
349
|
+
it('should maintain backward compatibility with createSession', async () => {
|
|
350
|
+
// Setup legacy config
|
|
351
|
+
vi.mocked(configurationManager.getCommandConfig).mockReturnValue({
|
|
352
|
+
command: 'claude',
|
|
353
|
+
args: ['--legacy'],
|
|
354
|
+
});
|
|
355
|
+
// Setup spawn mock
|
|
356
|
+
vi.mocked(spawn).mockReturnValue(mockPty);
|
|
357
|
+
// Create session using legacy method
|
|
358
|
+
await sessionManager.createSession('/test/worktree');
|
|
359
|
+
// Verify legacy method still works
|
|
360
|
+
expect(spawn).toHaveBeenCalledWith('claude', ['--legacy'], expect.any(Object));
|
|
361
|
+
});
|
|
362
|
+
});
|
|
260
363
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare class WorktreeConfigManager {
|
|
2
|
+
private static instance;
|
|
3
|
+
private isExtensionAvailable;
|
|
4
|
+
private constructor();
|
|
5
|
+
static getInstance(): WorktreeConfigManager;
|
|
6
|
+
initialize(gitPath?: string): void;
|
|
7
|
+
isAvailable(): boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const worktreeConfigManager: WorktreeConfigManager;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { isWorktreeConfigEnabled } from '../utils/worktreeConfig.js';
|
|
2
|
+
class WorktreeConfigManager {
|
|
3
|
+
constructor() {
|
|
4
|
+
Object.defineProperty(this, "isExtensionAvailable", {
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true,
|
|
8
|
+
value: null
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
static getInstance() {
|
|
12
|
+
if (!WorktreeConfigManager.instance) {
|
|
13
|
+
WorktreeConfigManager.instance = new WorktreeConfigManager();
|
|
14
|
+
}
|
|
15
|
+
return WorktreeConfigManager.instance;
|
|
16
|
+
}
|
|
17
|
+
initialize(gitPath) {
|
|
18
|
+
this.isExtensionAvailable = isWorktreeConfigEnabled(gitPath);
|
|
19
|
+
}
|
|
20
|
+
isAvailable() {
|
|
21
|
+
if (this.isExtensionAvailable === null) {
|
|
22
|
+
throw new Error('WorktreeConfigManager not initialized');
|
|
23
|
+
}
|
|
24
|
+
return this.isExtensionAvailable;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export const worktreeConfigManager = WorktreeConfigManager.getInstance();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
|
+
import { setWorktreeParentBranch } from '../utils/worktreeConfig.js';
|
|
4
5
|
export class WorktreeService {
|
|
5
6
|
constructor(rootPath) {
|
|
6
7
|
Object.defineProperty(this, "rootPath", {
|
|
@@ -201,6 +202,13 @@ export class WorktreeService {
|
|
|
201
202
|
cwd: this.gitRootPath, // Execute from git root to ensure proper resolution
|
|
202
203
|
encoding: 'utf8',
|
|
203
204
|
});
|
|
205
|
+
// Store the parent branch in worktree config
|
|
206
|
+
try {
|
|
207
|
+
setWorktreeParentBranch(resolvedPath, baseBranch);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
console.error('Warning: Failed to set parent branch in worktree config:', error);
|
|
211
|
+
}
|
|
204
212
|
return { success: true };
|
|
205
213
|
}
|
|
206
214
|
catch (error) {
|
|
@@ -3,6 +3,14 @@ import { WorktreeService } from './worktreeService.js';
|
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
4
|
// Mock child_process module
|
|
5
5
|
vi.mock('child_process');
|
|
6
|
+
// Mock worktreeConfigManager
|
|
7
|
+
vi.mock('./worktreeConfigManager.js', () => ({
|
|
8
|
+
worktreeConfigManager: {
|
|
9
|
+
initialize: vi.fn(),
|
|
10
|
+
isAvailable: vi.fn(() => true),
|
|
11
|
+
reset: vi.fn(),
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
6
14
|
// Get the mocked function with proper typing
|
|
7
15
|
const mockedExecSync = vi.mocked(execSync);
|
|
8
16
|
describe('WorktreeService', () => {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { IPty } from 'node-pty';
|
|
2
2
|
import type pkg from '@xterm/headless';
|
|
3
|
+
import { GitStatus } from '../utils/gitStatus.js';
|
|
3
4
|
export type Terminal = InstanceType<typeof pkg.Terminal>;
|
|
4
5
|
export type SessionState = 'idle' | 'busy' | 'waiting_input';
|
|
5
6
|
export interface Worktree {
|
|
@@ -7,6 +8,8 @@ export interface Worktree {
|
|
|
7
8
|
branch?: string;
|
|
8
9
|
isMainWorktree: boolean;
|
|
9
10
|
hasSession: boolean;
|
|
11
|
+
gitStatus?: GitStatus;
|
|
12
|
+
gitStatusError?: string;
|
|
10
13
|
}
|
|
11
14
|
export interface Session {
|
|
12
15
|
id: string;
|
|
@@ -58,9 +61,22 @@ export interface CommandConfig {
|
|
|
58
61
|
args?: string[];
|
|
59
62
|
fallbackArgs?: string[];
|
|
60
63
|
}
|
|
64
|
+
export interface CommandPreset {
|
|
65
|
+
id: string;
|
|
66
|
+
name: string;
|
|
67
|
+
command: string;
|
|
68
|
+
args?: string[];
|
|
69
|
+
fallbackArgs?: string[];
|
|
70
|
+
}
|
|
71
|
+
export interface CommandPresetsConfig {
|
|
72
|
+
presets: CommandPreset[];
|
|
73
|
+
defaultPresetId: string;
|
|
74
|
+
selectPresetOnStart?: boolean;
|
|
75
|
+
}
|
|
61
76
|
export interface ConfigurationData {
|
|
62
77
|
shortcuts?: ShortcutConfig;
|
|
63
78
|
statusHooks?: StatusHookConfig;
|
|
64
79
|
worktree?: WorktreeConfig;
|
|
65
80
|
command?: CommandConfig;
|
|
81
|
+
commandPresets?: CommandPresetsConfig;
|
|
66
82
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a function that limits concurrent executions
|
|
3
|
+
*/
|
|
4
|
+
export function createConcurrencyLimited(fn, maxConcurrent) {
|
|
5
|
+
if (maxConcurrent < 1) {
|
|
6
|
+
throw new RangeError('maxConcurrent must be at least 1');
|
|
7
|
+
}
|
|
8
|
+
let activeCount = 0;
|
|
9
|
+
const queue = [];
|
|
10
|
+
return async (...args) => {
|
|
11
|
+
// Wait for a slot if at capacity
|
|
12
|
+
if (activeCount >= maxConcurrent) {
|
|
13
|
+
await new Promise(resolve => {
|
|
14
|
+
queue.push(resolve);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
activeCount++;
|
|
18
|
+
try {
|
|
19
|
+
return await fn(...args);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
activeCount--;
|
|
23
|
+
// Release the next waiter in queue
|
|
24
|
+
const next = queue.shift();
|
|
25
|
+
if (next) {
|
|
26
|
+
next();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|