ccmanager 1.1.1 → 1.3.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.
package/README.md CHANGED
@@ -19,6 +19,7 @@ https://github.com/user-attachments/assets/15914a88-e288-4ac9-94d5-8127f2e19dbf
19
19
  - Command presets with automatic fallback support
20
20
  - Configurable state detection strategies for different CLI tools
21
21
  - Status change hooks for automation and notifications
22
+ - Devcontainer integration
22
23
 
23
24
  ## Why CCManager over Claude Squad?
24
25
 
@@ -177,6 +178,35 @@ CCManager can automatically generate worktree directory paths based on branch na
177
178
 
178
179
  For detailed configuration and examples, see [docs/worktree-auto-directory.md](docs/worktree-auto-directory.md).
179
180
 
181
+ ## Devcontainer Integration
182
+
183
+ CCManager supports running AI assistant sessions inside devcontainers while keeping the manager itself on the host machine. This enables sandboxed development environments with restricted network access while maintaining host-level notifications and automation.
184
+
185
+ ### Features
186
+
187
+ - **Host-based management**: CCManager runs on your host machine, managing sessions inside containers
188
+ - **Seamless integration**: All existing features (presets, status hooks, etc.) work with devcontainers
189
+ - **Security-focused**: Compatible with Anthropic's recommended devcontainer configurations
190
+ - **Persistent state**: Configuration and history persist across container recreations
191
+
192
+ ### Usage
193
+
194
+ ```bash
195
+ # Start CCManager with devcontainer support
196
+ npx ccmanager --devc-up-command "<your devcontainer up command>" \
197
+ --devc-exec-command "<your devcontainer exec command>"
198
+ ```
199
+
200
+ The devcontainer integration requires both commands:
201
+ - `--devc-up-command`: Any command to start the devcontainer
202
+ - `--devc-exec-command`: Any command to execute inside the container
203
+
204
+ ### Benefits
205
+
206
+ - **Safe experimentation**: Run commands like `claude --dangerously-skip-permissions` without risk
207
+
208
+ For detailed setup and configuration, see [docs/devcontainer.md](docs/devcontainer.md).
209
+
180
210
  ## Git Worktree Configuration
181
211
 
182
212
  CCManager can display enhanced git status information for each worktree when Git's worktree configuration extension is enabled.
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,9 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ export declare const parsedArgs: import("meow").Result<{
3
+ devcUpCommand: {
4
+ type: "string";
5
+ };
6
+ devcExecCommand: {
7
+ type: "string";
8
+ };
9
+ }>;
package/dist/cli.js CHANGED
@@ -4,19 +4,35 @@ import { render } from 'ink';
4
4
  import meow from 'meow';
5
5
  import App from './components/App.js';
6
6
  import { worktreeConfigManager } from './services/worktreeConfigManager.js';
7
- meow(`
7
+ const cli = meow(`
8
8
  Usage
9
9
  $ ccmanager
10
10
 
11
11
  Options
12
- --help Show help
13
- --version Show version
12
+ --help Show help
13
+ --version Show version
14
+ --devc-up-command Command to start devcontainer
15
+ --devc-exec-command Command to execute in devcontainer
14
16
 
15
17
  Examples
16
18
  $ ccmanager
19
+ $ ccmanager --devc-up-command "devcontainer up --workspace-folder ." --devc-exec-command "devcontainer exec --workspace-folder ."
17
20
  `, {
18
21
  importMeta: import.meta,
22
+ flags: {
23
+ devcUpCommand: {
24
+ type: 'string',
25
+ },
26
+ devcExecCommand: {
27
+ type: 'string',
28
+ },
29
+ },
19
30
  });
31
+ // Validate devcontainer arguments using XOR
32
+ if (!!cli.flags.devcUpCommand !== !!cli.flags.devcExecCommand) {
33
+ console.error('Error: Both --devc-up-command and --devc-exec-command must be provided together');
34
+ process.exit(1);
35
+ }
20
36
  // Check if we're in a TTY environment
21
37
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
22
38
  console.error('Error: ccmanager must be run in an interactive terminal (TTY)');
@@ -24,4 +40,15 @@ if (!process.stdin.isTTY || !process.stdout.isTTY) {
24
40
  }
25
41
  // Initialize worktree config manager
26
42
  worktreeConfigManager.initialize();
27
- render(React.createElement(App, null));
43
+ // Prepare devcontainer config
44
+ const devcontainerConfig = cli.flags.devcUpCommand && cli.flags.devcExecCommand
45
+ ? {
46
+ upCommand: cli.flags.devcUpCommand,
47
+ execCommand: cli.flags.devcExecCommand,
48
+ }
49
+ : undefined;
50
+ // Pass config to App
51
+ const appProps = devcontainerConfig ? { devcontainerConfig } : {};
52
+ render(React.createElement(App, { ...appProps }));
53
+ // Export for testing
54
+ export const parsedArgs = cli;
@@ -1,3 +1,7 @@
1
1
  import React from 'react';
2
- declare const App: React.FC;
2
+ import { DevcontainerConfig } from '../types/index.js';
3
+ interface AppProps {
4
+ devcontainerConfig?: DevcontainerConfig;
5
+ }
6
+ declare const App: React.FC<AppProps>;
3
7
  export default App;
@@ -11,7 +11,7 @@ import { SessionManager } from '../services/sessionManager.js';
11
11
  import { WorktreeService } from '../services/worktreeService.js';
12
12
  import { shortcutManager } from '../services/shortcutManager.js';
13
13
  import { configurationManager } from '../services/configurationManager.js';
14
- const App = () => {
14
+ const App = ({ devcontainerConfig }) => {
15
15
  const { exit } = useApp();
16
16
  const [view, setView] = useState('menu');
17
17
  const [sessionManager] = useState(() => new SessionManager());
@@ -87,7 +87,12 @@ const App = () => {
87
87
  }
88
88
  try {
89
89
  // Use preset-based session creation with default preset
90
- session = await sessionManager.createSessionWithPreset(worktree.path);
90
+ if (devcontainerConfig) {
91
+ session = await sessionManager.createSessionWithDevcontainer(worktree.path, devcontainerConfig);
92
+ }
93
+ else {
94
+ session = await sessionManager.createSessionWithPreset(worktree.path);
95
+ }
91
96
  }
92
97
  catch (error) {
93
98
  setError(`Failed to create session: ${error}`);
@@ -102,7 +107,13 @@ const App = () => {
102
107
  return;
103
108
  try {
104
109
  // Create session with selected preset
105
- const session = await sessionManager.createSessionWithPreset(selectedWorktree.path, presetId);
110
+ let session;
111
+ if (devcontainerConfig) {
112
+ session = await sessionManager.createSessionWithDevcontainer(selectedWorktree.path, devcontainerConfig, presetId);
113
+ }
114
+ else {
115
+ session = await sessionManager.createSessionWithPreset(selectedWorktree.path, presetId);
116
+ }
106
117
  setActiveSession(session);
107
118
  setView('session');
108
119
  setSelectedWorktree(null);
@@ -120,7 +131,7 @@ const App = () => {
120
131
  };
121
132
  const handleReturnToMenu = () => {
122
133
  setActiveSession(null);
123
- setError(null);
134
+ // Don't clear error here - let user dismiss it manually
124
135
  // Add a small delay to ensure Session cleanup completes
125
136
  setTimeout(() => {
126
137
  setView('menu');
@@ -139,11 +150,11 @@ const App = () => {
139
150
  }
140
151
  }, 50); // Small delay to ensure proper cleanup
141
152
  };
142
- const handleCreateWorktree = async (path, branch, baseBranch) => {
153
+ const handleCreateWorktree = async (path, branch, baseBranch, copyClaudeDirectory) => {
143
154
  setView('creating-worktree');
144
155
  setError(null);
145
156
  // Create the worktree
146
- const result = worktreeService.createWorktree(path, branch, baseBranch);
157
+ const result = worktreeService.createWorktree(path, branch, baseBranch, copyClaudeDirectory);
147
158
  if (result.success) {
148
159
  // Success - return to menu
149
160
  handleReturnToMenu();
@@ -210,7 +221,7 @@ const App = () => {
210
221
  handleReturnToMenu();
211
222
  };
212
223
  if (view === 'menu') {
213
- return (React.createElement(Menu, { key: menuKey, sessionManager: sessionManager, onSelectWorktree: handleSelectWorktree }));
224
+ return (React.createElement(Menu, { key: menuKey, sessionManager: sessionManager, onSelectWorktree: handleSelectWorktree, error: error, onDismissError: () => setError(null) }));
214
225
  }
215
226
  if (view === 'session' && activeSession) {
216
227
  return (React.createElement(Box, { flexDirection: "column" },
@@ -4,6 +4,8 @@ import { SessionManager } from '../services/sessionManager.js';
4
4
  interface MenuProps {
5
5
  sessionManager: SessionManager;
6
6
  onSelectWorktree: (worktree: Worktree) => void;
7
+ error?: string | null;
8
+ onDismissError?: () => void;
7
9
  }
8
10
  declare const Menu: React.FC<MenuProps>;
9
11
  export default Menu;
@@ -5,7 +5,7 @@ import { WorktreeService } from '../services/worktreeService.js';
5
5
  import { STATUS_ICONS, STATUS_LABELS, MENU_ICONS, } from '../constants/statusIcons.js';
6
6
  import { useGitStatus } from '../hooks/useGitStatus.js';
7
7
  import { prepareWorktreeItems, calculateColumnPositions, assembleWorktreeLabel, } from '../utils/worktreeUtils.js';
8
- const Menu = ({ sessionManager, onSelectWorktree }) => {
8
+ const Menu = ({ sessionManager, onSelectWorktree, error, onDismissError, }) => {
9
9
  const [baseWorktrees, setBaseWorktrees] = useState([]);
10
10
  const [defaultBranch, setDefaultBranch] = useState(null);
11
11
  const worktrees = useGitStatus(baseWorktrees, defaultBranch);
@@ -82,6 +82,11 @@ const Menu = ({ sessionManager, onSelectWorktree }) => {
82
82
  }, [worktrees, sessions, defaultBranch]);
83
83
  // Handle hotkeys
84
84
  useInput((input, _key) => {
85
+ // Dismiss error on any key press when error is shown
86
+ if (error && onDismissError) {
87
+ onDismissError();
88
+ return;
89
+ }
85
90
  const keyPressed = input.toLowerCase();
86
91
  // Handle number keys 0-9 for worktree selection (first 10 only)
87
92
  if (/^[0-9]$/.test(keyPressed)) {
@@ -198,7 +203,13 @@ const Menu = ({ sessionManager, onSelectWorktree }) => {
198
203
  React.createElement(Text, { bold: true, color: "green" }, "CCManager - Claude Code Worktree Manager")),
199
204
  React.createElement(Box, { marginBottom: 1 },
200
205
  React.createElement(Text, { dimColor: true }, "Select a worktree to start or resume a Claude Code session:")),
201
- React.createElement(SelectInput, { items: items, onSelect: handleSelect, isFocused: true }),
206
+ React.createElement(SelectInput, { items: items, onSelect: handleSelect, isFocused: !error }),
207
+ error && (React.createElement(Box, { marginTop: 1, paddingX: 1, borderStyle: "round", borderColor: "red" },
208
+ React.createElement(Box, { flexDirection: "column" },
209
+ React.createElement(Text, { color: "red", bold: true },
210
+ "Error: ",
211
+ error),
212
+ React.createElement(Text, { color: "gray", dimColor: true }, "Press any key to dismiss")))),
202
213
  React.createElement(Box, { marginTop: 1, flexDirection: "column" },
203
214
  React.createElement(Text, { dimColor: true },
204
215
  "Status: ",
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  interface NewWorktreeProps {
3
- onComplete: (path: string, branch: string, baseBranch: string) => void;
3
+ onComplete: (path: string, branch: string, baseBranch: string, copyClaudeDirectory: boolean) => void;
4
4
  onCancel: () => void;
5
5
  }
6
6
  declare const NewWorktree: React.FC<NewWorktreeProps>;
@@ -13,6 +13,7 @@ const NewWorktree = ({ onComplete, onCancel }) => {
13
13
  const [step, setStep] = useState(isAutoDirectory ? 'branch' : 'path');
14
14
  const [path, setPath] = useState('');
15
15
  const [branch, setBranch] = useState('');
16
+ const [baseBranch, setBaseBranch] = useState('');
16
17
  // Initialize worktree service and load branches (memoized to avoid re-initialization)
17
18
  const { branches, defaultBranch } = useMemo(() => {
18
19
  const service = new WorktreeService();
@@ -48,13 +49,31 @@ const NewWorktree = ({ onComplete, onCancel }) => {
48
49
  }
49
50
  };
50
51
  const handleBaseBranchSelect = (item) => {
52
+ setBaseBranch(item.value);
53
+ // Check if .claude directory exists in the base branch
54
+ const service = new WorktreeService();
55
+ if (service.hasClaudeDirectoryInBranch(item.value)) {
56
+ setStep('copy-settings');
57
+ }
58
+ else {
59
+ // Skip copy-settings step and complete with copySettings = false
60
+ if (isAutoDirectory) {
61
+ const autoPath = generateWorktreeDirectory(branch, worktreeConfig.autoDirectoryPattern);
62
+ onComplete(autoPath, branch, item.value, false);
63
+ }
64
+ else {
65
+ onComplete(path, branch, item.value, false);
66
+ }
67
+ }
68
+ };
69
+ const handleCopySettingsSelect = (item) => {
51
70
  if (isAutoDirectory) {
52
71
  // Generate path from branch name
53
72
  const autoPath = generateWorktreeDirectory(branch, worktreeConfig.autoDirectoryPattern);
54
- onComplete(autoPath, branch, item.value);
73
+ onComplete(autoPath, branch, baseBranch, item.value);
55
74
  }
56
75
  else {
57
- onComplete(path, branch, item.value);
76
+ onComplete(path, branch, baseBranch, item.value);
58
77
  }
59
78
  };
60
79
  // Calculate generated path for preview (memoized to avoid expensive recalculations)
@@ -97,6 +116,19 @@ const NewWorktree = ({ onComplete, onCancel }) => {
97
116
  React.createElement(Text, { color: "cyan" }, branch),
98
117
  ":")),
99
118
  React.createElement(SelectInput, { items: branchItems, onSelect: handleBaseBranchSelect, initialIndex: 0, limit: 10 }))),
119
+ step === 'copy-settings' && (React.createElement(Box, { flexDirection: "column" },
120
+ React.createElement(Box, { marginBottom: 1 },
121
+ React.createElement(Text, null,
122
+ "Copy .claude directory from base branch (",
123
+ React.createElement(Text, { color: "cyan" }, baseBranch),
124
+ ")?")),
125
+ React.createElement(SelectInput, { items: [
126
+ {
127
+ label: 'Yes - Copy .claude directory from base branch',
128
+ value: true,
129
+ },
130
+ { label: 'No - Start without .claude directory', value: false },
131
+ ], onSelect: handleCopySettingsSelect, initialIndex: 0 }))),
100
132
  React.createElement(Box, { marginTop: 1 },
101
133
  React.createElement(Text, { dimColor: true },
102
134
  "Press ",
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { SessionManager } from '../services/sessionManager.js';
3
+ import { spawn } from 'node-pty';
4
+ import { exec } from 'child_process';
5
+ // Mock modules
6
+ vi.mock('node-pty');
7
+ vi.mock('child_process');
8
+ vi.mock('util', () => ({
9
+ promisify: vi.fn((fn) => {
10
+ return (cmd, options) => {
11
+ return new Promise((resolve, reject) => {
12
+ const callback = (err, stdout, stderr) => {
13
+ if (err) {
14
+ reject(err);
15
+ }
16
+ else {
17
+ resolve({ stdout, stderr });
18
+ }
19
+ };
20
+ // Call the original function with the promisified callback
21
+ fn(cmd, options, callback);
22
+ });
23
+ };
24
+ }),
25
+ }));
26
+ vi.mock('../services/configurationManager.js', () => ({
27
+ configurationManager: {
28
+ getDefaultPreset: vi.fn(() => ({
29
+ id: 'claude',
30
+ name: 'Claude',
31
+ command: 'claude',
32
+ args: [],
33
+ })),
34
+ getPresetById: vi.fn(),
35
+ },
36
+ }));
37
+ vi.mock('../services/worktreeService.js', () => ({
38
+ WorktreeService: vi.fn(),
39
+ }));
40
+ const mockSpawn = vi.mocked(spawn);
41
+ const mockExec = vi.mocked(exec);
42
+ describe('Devcontainer Integration', () => {
43
+ let sessionManager;
44
+ let mockPty;
45
+ beforeEach(() => {
46
+ vi.clearAllMocks();
47
+ sessionManager = new SessionManager();
48
+ // Mock PTY process
49
+ mockPty = {
50
+ onData: vi.fn(),
51
+ onExit: vi.fn(),
52
+ write: vi.fn(),
53
+ resize: vi.fn(),
54
+ kill: vi.fn(),
55
+ pid: 12345,
56
+ cols: 80,
57
+ rows: 24,
58
+ process: 'claude',
59
+ };
60
+ mockSpawn.mockReturnValue(mockPty);
61
+ });
62
+ it('should execute devcontainer up command before creating session', async () => {
63
+ const devcontainerConfig = {
64
+ upCommand: 'devcontainer up --workspace-folder .',
65
+ execCommand: 'devcontainer exec --workspace-folder .',
66
+ };
67
+ mockExec.mockImplementation((cmd, options, callback) => {
68
+ if (typeof options === 'function') {
69
+ callback = options;
70
+ options = undefined;
71
+ }
72
+ if (callback && typeof callback === 'function') {
73
+ callback(null, 'Container started', '');
74
+ }
75
+ return {};
76
+ });
77
+ await sessionManager.createSessionWithDevcontainer('/test/worktree', devcontainerConfig);
78
+ expect(mockExec).toHaveBeenCalledWith(devcontainerConfig.upCommand, { cwd: '/test/worktree' }, expect.any(Function));
79
+ });
80
+ it('should handle devcontainer command execution with presets', async () => {
81
+ const devcontainerConfig = {
82
+ upCommand: 'devcontainer up --workspace-folder .',
83
+ execCommand: 'devcontainer exec --workspace-folder .',
84
+ };
85
+ mockExec.mockImplementation((cmd, options, callback) => {
86
+ if (typeof options === 'function') {
87
+ callback = options;
88
+ options = undefined;
89
+ }
90
+ if (callback && typeof callback === 'function') {
91
+ callback(null, 'Container started', '');
92
+ }
93
+ return {};
94
+ });
95
+ await sessionManager.createSessionWithDevcontainer('/test/worktree', devcontainerConfig);
96
+ // Should spawn with devcontainer exec command
97
+ expect(mockSpawn).toHaveBeenCalledWith('devcontainer', ['exec', '--workspace-folder', '.', '--', 'claude'], expect.objectContaining({
98
+ cwd: '/test/worktree',
99
+ }));
100
+ });
101
+ });
@@ -1,4 +1,4 @@
1
- import { Session, SessionManager as ISessionManager, SessionState } from '../types/index.js';
1
+ import { Session, SessionManager as ISessionManager, SessionState, DevcontainerConfig } from '../types/index.js';
2
2
  import { EventEmitter } from 'events';
3
3
  export declare class SessionManager extends EventEmitter implements ISessionManager {
4
4
  sessions: Map<string, Session>;
@@ -7,7 +7,9 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
7
7
  private spawn;
8
8
  detectTerminalState(session: Session): SessionState;
9
9
  constructor();
10
- createSession(worktreePath: string): Promise<Session>;
10
+ private createSessionId;
11
+ private createTerminal;
12
+ private createSessionInternal;
11
13
  createSessionWithPreset(worktreePath: string, presetId?: string): Promise<Session>;
12
14
  private setupDataHandler;
13
15
  private setupExitHandler;
@@ -18,5 +20,6 @@ export declare class SessionManager extends EventEmitter implements ISessionMana
18
20
  destroySession(worktreePath: string): void;
19
21
  getAllSessions(): Session[];
20
22
  private executeStatusHook;
23
+ createSessionWithDevcontainer(worktreePath: string, devcontainerConfig: DevcontainerConfig, presetId?: string): Promise<Session>;
21
24
  destroy(): void;
22
25
  }
@@ -2,10 +2,12 @@ import { spawn } from 'node-pty';
2
2
  import { EventEmitter } from 'events';
3
3
  import pkg from '@xterm/headless';
4
4
  import { exec } from 'child_process';
5
+ import { promisify } from 'util';
5
6
  import { configurationManager } from './configurationManager.js';
6
7
  import { WorktreeService } from './worktreeService.js';
7
8
  import { createStateDetector } from './stateDetector.js';
8
9
  const { Terminal } = pkg;
10
+ const execAsync = promisify(exec);
9
11
  export class SessionManager extends EventEmitter {
10
12
  async spawn(command, args, worktreePath) {
11
13
  const spawnOptions = {
@@ -45,27 +47,19 @@ export class SessionManager extends EventEmitter {
45
47
  });
46
48
  this.sessions = new Map();
47
49
  }
48
- async createSession(worktreePath) {
49
- // Check if session already exists
50
- const existing = this.sessions.get(worktreePath);
51
- if (existing) {
52
- return existing;
53
- }
54
- const id = `session-${Date.now()}-${Math.random()
55
- .toString(36)
56
- .substr(2, 9)}`;
57
- // Get command configuration
58
- const commandConfig = configurationManager.getCommandConfig();
59
- const command = commandConfig.command || 'claude';
60
- const args = commandConfig.args || [];
61
- // Spawn the process with fallback support
62
- const ptyProcess = await this.spawn(command, args, worktreePath);
63
- // Create virtual terminal for state detection
64
- const terminal = new Terminal({
50
+ createSessionId() {
51
+ return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
52
+ }
53
+ createTerminal() {
54
+ return new Terminal({
65
55
  cols: process.stdout.columns || 80,
66
56
  rows: process.stdout.rows || 24,
67
57
  allowProposedApi: true,
68
58
  });
59
+ }
60
+ async createSessionInternal(worktreePath, ptyProcess, commandConfig, options = {}) {
61
+ const id = this.createSessionId();
62
+ const terminal = this.createTerminal();
69
63
  const session = {
70
64
  id,
71
65
  worktreePath,
@@ -76,9 +70,10 @@ export class SessionManager extends EventEmitter {
76
70
  lastActivity: new Date(),
77
71
  isActive: false,
78
72
  terminal,
79
- isPrimaryCommand: true,
73
+ isPrimaryCommand: options.isPrimaryCommand ?? true,
80
74
  commandConfig,
81
- detectionStrategy: 'claude', // Default to claude for legacy method
75
+ detectionStrategy: options.detectionStrategy ?? 'claude',
76
+ devcontainerConfig: options.devcontainerConfig,
82
77
  };
83
78
  // Set up persistent background data handler for state detection
84
79
  this.setupBackgroundHandler(session);
@@ -92,9 +87,6 @@ export class SessionManager extends EventEmitter {
92
87
  if (existing) {
93
88
  return existing;
94
89
  }
95
- const id = `session-${Date.now()}-${Math.random()
96
- .toString(36)
97
- .substr(2, 9)}`;
98
90
  // Get preset configuration
99
91
  let preset = presetId ? configurationManager.getPresetById(presetId) : null;
100
92
  if (!preset) {
@@ -130,31 +122,10 @@ export class SessionManager extends EventEmitter {
130
122
  throw error;
131
123
  }
132
124
  }
133
- // Create virtual terminal for state detection
134
- const terminal = new Terminal({
135
- cols: process.stdout.columns || 80,
136
- rows: process.stdout.rows || 24,
137
- allowProposedApi: true,
138
- });
139
- const session = {
140
- id,
141
- worktreePath,
142
- process: ptyProcess,
143
- state: 'busy', // Session starts as busy when created
144
- output: [],
145
- outputHistory: [],
146
- lastActivity: new Date(),
147
- isActive: false,
148
- terminal,
125
+ return this.createSessionInternal(worktreePath, ptyProcess, commandConfig, {
149
126
  isPrimaryCommand,
150
- commandConfig,
151
- detectionStrategy: preset.detectionStrategy || 'claude',
152
- };
153
- // Set up persistent background data handler for state detection
154
- this.setupBackgroundHandler(session);
155
- this.sessions.set(worktreePath, session);
156
- this.emit('sessionCreated', session);
157
- return session;
127
+ detectionStrategy: preset.detectionStrategy,
128
+ });
158
129
  }
159
130
  setupDataHandler(session) {
160
131
  // This handler always runs for all data
@@ -304,6 +275,48 @@ export class SessionManager extends EventEmitter {
304
275
  });
305
276
  }
306
277
  }
278
+ async createSessionWithDevcontainer(worktreePath, devcontainerConfig, presetId) {
279
+ // Check if session already exists
280
+ const existing = this.sessions.get(worktreePath);
281
+ if (existing) {
282
+ return existing;
283
+ }
284
+ // Execute devcontainer up command first
285
+ try {
286
+ await execAsync(devcontainerConfig.upCommand, { cwd: worktreePath });
287
+ }
288
+ catch (error) {
289
+ throw new Error(`Failed to start devcontainer: ${error instanceof Error ? error.message : String(error)}`);
290
+ }
291
+ // Get preset configuration
292
+ let preset = presetId ? configurationManager.getPresetById(presetId) : null;
293
+ if (!preset) {
294
+ preset = configurationManager.getDefaultPreset();
295
+ }
296
+ // Parse the exec command to extract arguments
297
+ const execParts = devcontainerConfig.execCommand.split(/\s+/);
298
+ const devcontainerCmd = execParts[0] || 'devcontainer'; // Should be 'devcontainer'
299
+ const execArgs = execParts.slice(1); // Rest of the exec command args
300
+ // Build the full command: devcontainer exec [args] -- [preset command] [preset args]
301
+ const fullArgs = [
302
+ ...execArgs,
303
+ '--',
304
+ preset.command,
305
+ ...(preset.args || []),
306
+ ];
307
+ // Spawn the process within devcontainer
308
+ const ptyProcess = await this.spawn(devcontainerCmd, fullArgs, worktreePath);
309
+ const commandConfig = {
310
+ command: preset.command,
311
+ args: preset.args,
312
+ fallbackArgs: preset.fallbackArgs,
313
+ };
314
+ return this.createSessionInternal(worktreePath, ptyProcess, commandConfig, {
315
+ isPrimaryCommand: true,
316
+ detectionStrategy: preset.detectionStrategy,
317
+ devcontainerConfig,
318
+ });
319
+ }
307
320
  destroy() {
308
321
  // Clean up all sessions
309
322
  for (const worktreePath of this.sessions.keys()) {