ccmanager 0.2.0 → 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.
@@ -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
  });
@@ -61,9 +61,22 @@ export interface CommandConfig {
61
61
  args?: string[];
62
62
  fallbackArgs?: string[];
63
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
+ }
64
76
  export interface ConfigurationData {
65
77
  shortcuts?: ShortcutConfig;
66
78
  statusHooks?: StatusHookConfig;
67
79
  worktree?: WorktreeConfig;
68
80
  command?: CommandConfig;
81
+ commandPresets?: CommandPresetsConfig;
69
82
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",