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
@@ -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', () => {
@@ -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,4 @@
1
+ /**
2
+ * Create a function that limits concurrent executions
3
+ */
4
+ export declare function createConcurrencyLimited<TArgs extends unknown[], TResult>(fn: (...args: TArgs) => Promise<TResult>, maxConcurrent: number): (...args: TArgs) => Promise<TResult>;
@@ -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 {};