@proletariat/cli 0.3.9 → 0.3.11
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 +25 -0
- package/bin/dev.js +0 -0
- package/dist/commands/action/index.js +1 -1
- package/dist/commands/action/run.js +8 -12
- package/dist/commands/agent/auth.d.ts +30 -0
- package/dist/commands/agent/auth.js +172 -0
- package/dist/commands/agent/discover.d.ts +9 -0
- package/dist/commands/agent/discover.js +67 -0
- package/dist/commands/agent/index.js +47 -12
- package/dist/commands/agent/list.d.ts +4 -1
- package/dist/commands/agent/list.js +78 -16
- package/dist/commands/agent/login.js +35 -31
- package/dist/commands/agent/restart.js +2 -0
- package/dist/commands/agent/shell.js +78 -19
- package/dist/commands/agent/staff/add.js +1 -12
- package/dist/commands/agent/staff/remove.js +9 -7
- package/dist/commands/agent/status.js +17 -4
- package/dist/commands/agent/temp/cleanup.js +7 -3
- package/dist/commands/agent/themes/index.js +4 -5
- package/dist/commands/agent/themes/list.js +5 -5
- package/dist/commands/agent/visit.js +17 -4
- package/dist/commands/branch/create.d.ts +4 -0
- package/dist/commands/branch/create.js +16 -8
- package/dist/commands/branch/index.js +1 -1
- package/dist/commands/branch/where.js +1 -0
- package/dist/commands/claude.d.ts +38 -0
- package/dist/commands/claude.js +899 -0
- package/dist/commands/commit.js +1 -1
- package/dist/commands/config/index.d.ts +12 -0
- package/dist/commands/config/index.js +271 -0
- package/dist/commands/docker/clean.js +2 -2
- package/dist/commands/docker/index.js +2 -2
- package/dist/commands/docker/list.js +3 -8
- package/dist/commands/docker/logs.js +2 -2
- package/dist/commands/docker/prune.js +1 -1
- package/dist/commands/docker/restart.js +2 -2
- package/dist/commands/docker/shell.js +2 -2
- package/dist/commands/docker/start.js +2 -2
- package/dist/commands/docker/status.js +1 -1
- package/dist/commands/docker/stop.js +2 -2
- package/dist/commands/docker/sync.js +2 -2
- package/dist/commands/epic/index.js +1 -1
- package/dist/commands/epic/link/index.js +25 -14
- package/dist/commands/epic/link/remove.js +2 -0
- package/dist/commands/epic/list.js +5 -5
- package/dist/commands/epic/progress.js +10 -4
- package/dist/commands/epic/spec.js +2 -0
- package/dist/commands/epic/ticket.js +3 -0
- package/dist/commands/execution/stop.js +1 -0
- package/dist/commands/init.js +4 -4
- package/dist/commands/project/index.js +1 -1
- package/dist/commands/project/spec.js +7 -0
- package/dist/commands/repo/add.js +1 -0
- package/dist/commands/repo/remove.js +1 -0
- package/dist/commands/roadmap/add-project.d.ts +18 -0
- package/dist/commands/roadmap/add-project.js +135 -0
- package/dist/commands/roadmap/create.d.ts +22 -0
- package/dist/commands/roadmap/create.js +156 -0
- package/dist/commands/roadmap/delete.d.ts +17 -0
- package/dist/commands/roadmap/delete.js +104 -0
- package/dist/commands/roadmap/generate.d.ts +22 -0
- package/dist/commands/roadmap/generate.js +201 -0
- package/dist/commands/roadmap/index.d.ts +13 -0
- package/dist/commands/roadmap/index.js +61 -0
- package/dist/commands/roadmap/list.d.ts +12 -0
- package/dist/commands/roadmap/list.js +42 -0
- package/dist/commands/roadmap/remove-project.d.ts +18 -0
- package/dist/commands/roadmap/remove-project.js +147 -0
- package/dist/commands/roadmap/reorder.d.ts +17 -0
- package/dist/commands/roadmap/reorder.js +157 -0
- package/dist/commands/roadmap/update.d.ts +19 -0
- package/dist/commands/roadmap/update.js +136 -0
- package/dist/commands/roadmap/view.d.ts +16 -0
- package/dist/commands/roadmap/view.js +103 -0
- package/dist/commands/spec/index.js +1 -1
- package/dist/commands/spec/link/index.js +24 -13
- package/dist/commands/spec/link/remove.js +2 -0
- package/dist/commands/status/index.js +1 -1
- package/dist/commands/status/list.js +0 -8
- package/dist/commands/template/delete.js +2 -0
- package/dist/commands/terminal/title.d.ts +12 -0
- package/dist/commands/terminal/title.js +48 -0
- package/dist/commands/ticket/complete.js +2 -0
- package/dist/commands/ticket/create.js +4 -2
- package/dist/commands/ticket/delete.js +2 -0
- package/dist/commands/ticket/edit.js +8 -2
- package/dist/commands/ticket/link/index.js +17 -3
- package/dist/commands/ticket/link/remove.js +2 -0
- package/dist/commands/ticket/list.js +1 -2
- package/dist/commands/ticket/move.js +2 -0
- package/dist/commands/ticket/project.js +3 -1
- package/dist/commands/ticket/reassign.js +2 -0
- package/dist/commands/ticket/spec.js +4 -2
- package/dist/commands/ticket/template/apply.js +4 -3
- package/dist/commands/ticket/template/create.js +2 -0
- package/dist/commands/ticket/template/index.js +1 -1
- package/dist/commands/ticket/update.js +2 -0
- package/dist/commands/work/index.js +1 -1
- package/dist/commands/work/revise.js +7 -1
- package/dist/commands/work/spawn.d.ts +2 -1
- package/dist/commands/work/spawn.js +131 -36
- package/dist/commands/work/start.d.ts +2 -1
- package/dist/commands/work/start.js +349 -69
- package/dist/commands/work/watch.js +10 -2
- package/dist/commands/workflow/create.js +3 -3
- package/dist/commands/workflow/switch.js +2 -1
- package/dist/commands/workspace/remove.js +0 -8
- package/dist/commands/workspace/use.js +1 -9
- package/dist/lib/agents/commands.js +18 -13
- package/dist/lib/database/index.d.ts +19 -12
- package/dist/lib/database/index.js +158 -42
- package/dist/lib/docker/resolve.js +1 -1
- package/dist/lib/execution/config.d.ts +6 -0
- package/dist/lib/execution/config.js +15 -2
- package/dist/lib/execution/devcontainer.d.ts +2 -0
- package/dist/lib/execution/devcontainer.js +41 -9
- package/dist/lib/execution/runners.d.ts +85 -3
- package/dist/lib/execution/runners.js +925 -228
- package/dist/lib/execution/spawner.d.ts +2 -2
- package/dist/lib/execution/spawner.js +4 -3
- package/dist/lib/execution/storage.d.ts +2 -1
- package/dist/lib/execution/storage.js +9 -13
- package/dist/lib/execution/types.d.ts +10 -1
- package/dist/lib/execution/types.js +3 -1
- package/dist/lib/init/index.js +1 -0
- package/dist/lib/machine-config.js +1 -1
- package/dist/lib/pmo/base-command.js +5 -9
- package/dist/lib/pmo/index.js +2 -0
- package/dist/lib/pmo/schema.d.ts +6 -0
- package/dist/lib/pmo/schema.js +36 -0
- package/dist/lib/pmo/storage/base.js +3 -3
- package/dist/lib/pmo/storage/index.d.ts +16 -1
- package/dist/lib/pmo/storage/index.js +45 -0
- package/dist/lib/pmo/storage/roadmaps.d.ts +62 -0
- package/dist/lib/pmo/storage/roadmaps.js +301 -0
- package/dist/lib/pmo/storage/specs.js +2 -0
- package/dist/lib/pmo/storage/types.d.ts +14 -0
- package/dist/lib/pmo/sync-manager.d.ts +1 -1
- package/dist/lib/pmo/sync-manager.js +1 -1
- package/dist/lib/pmo/types.d.ts +41 -0
- package/dist/lib/pmo/utils.d.ts +2 -0
- package/dist/lib/pmo/utils.js +22 -1
- package/dist/lib/repos/index.js +7 -1
- package/dist/lib/terminal.d.ts +31 -0
- package/dist/lib/terminal.js +48 -0
- package/dist/lib/themes.d.ts +21 -3
- package/dist/lib/themes.js +80 -23
- package/dist/lib/workspace-config.d.ts +80 -0
- package/dist/lib/workspace-config.js +100 -0
- package/oclif.manifest.json +4065 -3225
- package/package.json +10 -6
- package/LICENSE +0 -21
|
@@ -8,6 +8,7 @@ import * as fs from 'node:fs';
|
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import * as os from 'node:os';
|
|
10
10
|
import { DEFAULT_EXECUTION_CONFIG, } from './types.js';
|
|
11
|
+
import { getSetTitleCommands } from '../terminal.js';
|
|
11
12
|
// =============================================================================
|
|
12
13
|
// Terminal Title Helpers
|
|
13
14
|
// =============================================================================
|
|
@@ -30,21 +31,116 @@ function buildTmuxWindowName(context) {
|
|
|
30
31
|
return buildSessionName(context);
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
34
|
+
* Check if tmux control mode (-CC) should be used.
|
|
35
|
+
* Control mode is only used with iTerm when controlMode is enabled in config.
|
|
35
36
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* When control mode is active:
|
|
38
|
+
* - iTerm handles scrolling, selection, and gestures natively
|
|
39
|
+
* - tmux mouse mode should be disabled to avoid conflicts
|
|
39
40
|
*/
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
export function shouldUseControlMode(terminalApp, controlModeEnabled) {
|
|
42
|
+
return terminalApp === 'iTerm' && controlModeEnabled;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build the tmux mouse option string for session creation.
|
|
46
|
+
* Enables mouse mode for scroll support in tmux.
|
|
47
|
+
* To select text or switch tabs, hold Shift or Option to bypass tmux.
|
|
48
|
+
*/
|
|
49
|
+
export function buildTmuxMouseOption(_useControlMode) {
|
|
50
|
+
return ' \\; set-option -g mouse on';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the tmux attach command based on control mode.
|
|
54
|
+
* Uses -u -CC flags for iTerm control mode (native scrolling/selection).
|
|
55
|
+
* -u forces UTF-8 mode which is required for proper iTerm integration.
|
|
56
|
+
* Uses regular attach otherwise.
|
|
57
|
+
*/
|
|
58
|
+
export function buildTmuxAttachCommand(useControlMode, includeUnicodeFlag = false) {
|
|
59
|
+
const unicodeFlag = includeUnicodeFlag ? '-u ' : '';
|
|
60
|
+
if (useControlMode) {
|
|
61
|
+
// Always use -u with -CC for proper iTerm integration
|
|
62
|
+
return `tmux -u -CC attach`;
|
|
63
|
+
}
|
|
64
|
+
return `tmux ${unicodeFlag}attach`;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Configure iTerm tmux preferences for control mode.
|
|
68
|
+
* - windowMode: whether tmux -CC opens windows as tabs or new windows
|
|
69
|
+
* - autoHide: automatically bury/hide the control session (the terminal where -CC was run)
|
|
70
|
+
* @param mode - 'tab' for tabs in current window, 'window' for new windows
|
|
71
|
+
*/
|
|
72
|
+
export function configureITermTmuxPreferences(mode) {
|
|
73
|
+
try {
|
|
74
|
+
// OpenTmuxWindowsIn: 0=native windows, 1=new window, 2=tabs in existing window
|
|
75
|
+
const windowModeValue = mode === 'tab' ? 2 : 1;
|
|
76
|
+
execSync(`defaults write com.googlecode.iterm2 OpenTmuxWindowsIn -int ${windowModeValue}`, { stdio: 'pipe' });
|
|
77
|
+
// AutoHideTmuxClientSession: hide the control channel terminal so it doesn't clutter
|
|
78
|
+
execSync(`defaults write com.googlecode.iterm2 AutoHideTmuxClientSession -bool true`, { stdio: 'pipe' });
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Non-fatal - preference setting failed but execution can continue
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Legacy alias for backwards compatibility
|
|
85
|
+
export function configureITermTmuxWindowMode(mode) {
|
|
86
|
+
configureITermTmuxPreferences(mode);
|
|
87
|
+
}
|
|
88
|
+
// =============================================================================
|
|
89
|
+
// Docker Credential Helpers
|
|
90
|
+
// =============================================================================
|
|
91
|
+
const CLAUDE_CREDENTIALS_VOLUME = 'claude-credentials';
|
|
92
|
+
/**
|
|
93
|
+
* Check if the claude-credentials Docker volume exists.
|
|
94
|
+
*/
|
|
95
|
+
export function credentialsVolumeExists() {
|
|
96
|
+
try {
|
|
97
|
+
execSync(`docker volume inspect ${CLAUDE_CREDENTIALS_VOLUME}`, { stdio: 'pipe' });
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if valid Claude credentials exist in the Docker volume.
|
|
106
|
+
* Returns true if credentials exist and are not expired.
|
|
107
|
+
*/
|
|
108
|
+
export function dockerCredentialsExist() {
|
|
109
|
+
try {
|
|
110
|
+
const result = execSync(`docker run --rm -v ${CLAUDE_CREDENTIALS_VOLUME}:/data alpine cat /data/.credentials.json 2>/dev/null`, { stdio: 'pipe', encoding: 'utf-8' });
|
|
111
|
+
const creds = JSON.parse(result);
|
|
112
|
+
if (creds.claudeAiOauth?.accessToken && creds.claudeAiOauth?.expiresAt) {
|
|
113
|
+
// Check if expired
|
|
114
|
+
const expiresAt = creds.claudeAiOauth.expiresAt;
|
|
115
|
+
if (expiresAt > Date.now()) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Get Docker credential info for display.
|
|
127
|
+
* Returns expiration date and subscription type if available.
|
|
128
|
+
*/
|
|
129
|
+
export function getDockerCredentialInfo() {
|
|
130
|
+
try {
|
|
131
|
+
const result = execSync(`docker run --rm -v ${CLAUDE_CREDENTIALS_VOLUME}:/data alpine cat /data/.credentials.json 2>/dev/null`, { stdio: 'pipe', encoding: 'utf-8' });
|
|
132
|
+
const creds = JSON.parse(result);
|
|
133
|
+
if (creds.claudeAiOauth?.expiresAt) {
|
|
134
|
+
return {
|
|
135
|
+
expiresAt: new Date(creds.claudeAiOauth.expiresAt),
|
|
136
|
+
subscriptionType: creds.claudeAiOauth.subscriptionType,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
48
144
|
}
|
|
49
145
|
// =============================================================================
|
|
50
146
|
// Executor Commands
|
|
@@ -55,7 +151,9 @@ function getExecutorCommand(executor, prompt, skipPermissions = true) {
|
|
|
55
151
|
if (skipPermissions) {
|
|
56
152
|
// Skip permissions - agent runs autonomously without prompting
|
|
57
153
|
// Note: NO -p flag - we want interactive mode for streaming output in terminal
|
|
58
|
-
|
|
154
|
+
// --permission-mode bypassPermissions: skips the "trust this folder" dialog
|
|
155
|
+
// --dangerously-skip-permissions: skips tool permission checks
|
|
156
|
+
return { cmd: 'claude', args: ['--permission-mode', 'bypassPermissions', '--dangerously-skip-permissions', prompt] };
|
|
59
157
|
}
|
|
60
158
|
// Manual mode - will prompt for each action (still interactive, no -p)
|
|
61
159
|
return { cmd: 'claude', args: [prompt] };
|
|
@@ -69,7 +167,7 @@ function getExecutorCommand(executor, prompt, skipPermissions = true) {
|
|
|
69
167
|
default:
|
|
70
168
|
if (skipPermissions) {
|
|
71
169
|
// Note: NO -p flag - we want interactive mode for streaming output
|
|
72
|
-
return { cmd: 'claude', args: ['--dangerously-skip-permissions', prompt] };
|
|
170
|
+
return { cmd: 'claude', args: ['--permission-mode', 'bypassPermissions', '--dangerously-skip-permissions', prompt] };
|
|
73
171
|
}
|
|
74
172
|
return { cmd: 'claude', args: [prompt] };
|
|
75
173
|
}
|
|
@@ -225,9 +323,14 @@ exec $SHELL
|
|
|
225
323
|
// Check if tmux is available
|
|
226
324
|
execSync('which tmux', { stdio: 'pipe' });
|
|
227
325
|
const terminalApp = config.terminal.app;
|
|
326
|
+
// Check if we should use iTerm control mode (-CC)
|
|
327
|
+
// When using -CC, iTerm handles scrolling/selection natively, so we DON'T set mouse on
|
|
328
|
+
// Without -CC, we need mouse on for tmux to handle scrolling
|
|
329
|
+
const useControlMode = shouldUseControlMode(terminalApp, config.tmux.controlMode);
|
|
228
330
|
// Step 1: Create host tmux session (detached)
|
|
229
|
-
//
|
|
230
|
-
const
|
|
331
|
+
// Only enable mouse mode if NOT using control mode (control mode lets iTerm handle mouse natively)
|
|
332
|
+
const mouseOption = buildTmuxMouseOption(useControlMode);
|
|
333
|
+
const tmuxCmd = `tmux new-session -d -s "${sessionName}" -n "${sessionName}" "${scriptPath}"${mouseOption} \\; set-option -g set-titles on \\; set-option -g set-titles-string "#{window_name}"`;
|
|
231
334
|
try {
|
|
232
335
|
execSync(tmuxCmd, { stdio: 'pipe' });
|
|
233
336
|
}
|
|
@@ -237,45 +340,143 @@ exec $SHELL
|
|
|
237
340
|
error: `Failed to create tmux session: ${error instanceof Error ? error.message : error}`,
|
|
238
341
|
};
|
|
239
342
|
}
|
|
240
|
-
// Step 2: Open terminal tab attached to tmux session (unless background mode)
|
|
343
|
+
// Step 2: Open terminal tab attached to tmux session (unless background or foreground mode)
|
|
241
344
|
if (displayMode === 'background') {
|
|
242
345
|
return {
|
|
243
346
|
success: true,
|
|
244
347
|
sessionId: sessionName,
|
|
245
348
|
};
|
|
246
349
|
}
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
350
|
+
// Foreground mode: attach to tmux session in current terminal (blocking)
|
|
351
|
+
if (displayMode === 'foreground') {
|
|
352
|
+
try {
|
|
353
|
+
// Clear screen and attach - this blocks until user detaches or claude exits
|
|
354
|
+
// Use -CC for iTerm when control mode is enabled
|
|
355
|
+
const fgTmuxAttach = buildTmuxAttachCommand(useControlMode);
|
|
356
|
+
execSync(`clear && ${fgTmuxAttach} -t "${sessionName}"`, { stdio: 'inherit' });
|
|
357
|
+
return {
|
|
358
|
+
success: true,
|
|
359
|
+
sessionId: sessionName,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
return {
|
|
364
|
+
success: false,
|
|
365
|
+
error: `Failed to attach to tmux session: ${error instanceof Error ? error.message : error}`,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Use tmux -CC (control mode) for iTerm when enabled in config
|
|
370
|
+
// -CC gives native iTerm scrolling, selection, and gesture support
|
|
371
|
+
// Without -CC, use regular attach (relies on mouse mode for scrolling)
|
|
372
|
+
const tmuxAttach = buildTmuxAttachCommand(useControlMode);
|
|
373
|
+
const attachCmd = `clear && ${tmuxAttach} -t \\"${sessionName}\\"`;
|
|
374
|
+
// For iTerm with control mode, create a new tab and run -CC attach there
|
|
375
|
+
// This avoids interfering with the terminal where prlt is running
|
|
376
|
+
if (terminalApp === 'iTerm' && useControlMode) {
|
|
377
|
+
// Configure iTerm to open tmux windows as tabs or windows based on user preference
|
|
378
|
+
configureITermTmuxWindowMode(config.tmux.windowMode);
|
|
379
|
+
const openInBackground = config.terminal.openInBackground ?? true;
|
|
380
|
+
if (openInBackground) {
|
|
381
|
+
// Open tab without stealing focus - save frontmost app and restore after
|
|
382
|
+
execSync(`osascript -e '
|
|
383
|
+
set frontApp to path to frontmost application as text
|
|
384
|
+
tell application "iTerm"
|
|
385
|
+
tell current window
|
|
386
|
+
set newTab to (create tab with default profile)
|
|
387
|
+
tell current session of newTab
|
|
388
|
+
write text "tmux -u -CC attach -t \\"${sessionName}\\""
|
|
389
|
+
end tell
|
|
390
|
+
end tell
|
|
391
|
+
end tell
|
|
392
|
+
tell application frontApp to activate
|
|
393
|
+
'`);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
257
396
|
execSync(`osascript -e '
|
|
258
397
|
tell application "iTerm"
|
|
259
398
|
activate
|
|
260
|
-
|
|
261
|
-
create
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
set name to "${windowTitle}"
|
|
265
|
-
write text "${attachCmd}"
|
|
399
|
+
tell current window
|
|
400
|
+
set newTab to (create tab with default profile)
|
|
401
|
+
tell current session of newTab
|
|
402
|
+
write text "tmux -u -CC attach -t \\"${sessionName}\\""
|
|
266
403
|
end tell
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
404
|
+
end tell
|
|
405
|
+
end tell
|
|
406
|
+
'`);
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
success: true,
|
|
410
|
+
sessionId: sessionName,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
// Check if we should open in background (don't steal focus)
|
|
414
|
+
const openInBackground = config.terminal.openInBackground ?? true;
|
|
415
|
+
switch (terminalApp) {
|
|
416
|
+
case 'iTerm':
|
|
417
|
+
// Without control mode, create a new tab and attach normally
|
|
418
|
+
// When openInBackground is true, save frontmost app and restore after
|
|
419
|
+
if (openInBackground) {
|
|
420
|
+
execSync(`osascript -e '
|
|
421
|
+
-- Save the currently active application and window
|
|
422
|
+
tell application "System Events"
|
|
423
|
+
set frontApp to name of first application process whose frontmost is true
|
|
424
|
+
set frontAppBundle to bundle identifier of first application process whose frontmost is true
|
|
425
|
+
end tell
|
|
426
|
+
|
|
427
|
+
tell application "iTerm"
|
|
428
|
+
if (count of windows) = 0 then
|
|
429
|
+
create window with default profile
|
|
270
430
|
delay 0.3
|
|
271
|
-
tell current session of
|
|
431
|
+
tell current session of current window
|
|
272
432
|
set name to "${windowTitle}"
|
|
273
433
|
write text "${attachCmd}"
|
|
274
434
|
end tell
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
435
|
+
else
|
|
436
|
+
tell current window
|
|
437
|
+
set newTab to (create tab with default profile)
|
|
438
|
+
delay 0.3
|
|
439
|
+
tell current session of newTab
|
|
440
|
+
set name to "${windowTitle}"
|
|
441
|
+
write text "${attachCmd}"
|
|
442
|
+
end tell
|
|
443
|
+
end tell
|
|
444
|
+
end if
|
|
445
|
+
end tell
|
|
446
|
+
|
|
447
|
+
-- Restore focus to the original application
|
|
448
|
+
delay 0.2
|
|
449
|
+
tell application "System Events"
|
|
450
|
+
set frontmost of process frontApp to true
|
|
451
|
+
end tell
|
|
452
|
+
delay 0.1
|
|
453
|
+
do shell script "open -b " & quoted form of frontAppBundle
|
|
454
|
+
'`);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
execSync(`osascript -e '
|
|
458
|
+
tell application "iTerm"
|
|
459
|
+
activate
|
|
460
|
+
if (count of windows) = 0 then
|
|
461
|
+
create window with default profile
|
|
462
|
+
delay 0.3
|
|
463
|
+
tell current session of current window
|
|
464
|
+
set name to "${windowTitle}"
|
|
465
|
+
write text "${attachCmd}"
|
|
466
|
+
end tell
|
|
467
|
+
else
|
|
468
|
+
tell current window
|
|
469
|
+
set newTab to (create tab with default profile)
|
|
470
|
+
delay 0.3
|
|
471
|
+
tell current session of newTab
|
|
472
|
+
set name to "${windowTitle}"
|
|
473
|
+
write text "${attachCmd}"
|
|
474
|
+
end tell
|
|
475
|
+
end tell
|
|
476
|
+
end if
|
|
477
|
+
end tell
|
|
478
|
+
'`);
|
|
479
|
+
}
|
|
279
480
|
break;
|
|
280
481
|
case 'Ghostty':
|
|
281
482
|
// Ghostty - use osascript to open new tab and run command
|
|
@@ -320,18 +521,32 @@ exec $SHELL
|
|
|
320
521
|
case 'Terminal':
|
|
321
522
|
default:
|
|
322
523
|
// macOS Terminal.app - new tab
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
524
|
+
// Note: Terminal.app with System Events keystrokes requires activation for Cmd+T
|
|
525
|
+
// But we can use 'do script' which opens a new window without activation if needed
|
|
526
|
+
if (openInBackground) {
|
|
527
|
+
// Open in background: use 'do script' which creates a new window without activating
|
|
528
|
+
execSync(`osascript -e '
|
|
529
|
+
tell application "Terminal"
|
|
530
|
+
do script "${attachCmd}"
|
|
531
|
+
set custom title of front window to "${windowTitle}"
|
|
532
|
+
end tell
|
|
533
|
+
'`);
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
// Bring to front: use traditional Cmd+T for new tab
|
|
537
|
+
execSync(`osascript -e '
|
|
538
|
+
tell application "Terminal"
|
|
539
|
+
activate
|
|
540
|
+
tell application "System Events"
|
|
541
|
+
tell process "Terminal"
|
|
542
|
+
keystroke "t" using command down
|
|
543
|
+
end tell
|
|
329
544
|
end tell
|
|
545
|
+
delay 0.3
|
|
546
|
+
do script "${attachCmd}" in front window
|
|
330
547
|
end tell
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
end tell
|
|
334
|
-
'`);
|
|
548
|
+
'`);
|
|
549
|
+
}
|
|
335
550
|
break;
|
|
336
551
|
}
|
|
337
552
|
return {
|
|
@@ -347,6 +562,41 @@ exec $SHELL
|
|
|
347
562
|
}
|
|
348
563
|
}
|
|
349
564
|
// =============================================================================
|
|
565
|
+
// GitHub Token Check
|
|
566
|
+
// =============================================================================
|
|
567
|
+
/**
|
|
568
|
+
* Check if GitHub token is available for git push operations.
|
|
569
|
+
* Checks environment variables first, then tries gh auth token.
|
|
570
|
+
* Returns the token if available, null otherwise.
|
|
571
|
+
*/
|
|
572
|
+
export function getGitHubToken() {
|
|
573
|
+
// Check environment variables first
|
|
574
|
+
if (process.env.GITHUB_TOKEN) {
|
|
575
|
+
return process.env.GITHUB_TOKEN;
|
|
576
|
+
}
|
|
577
|
+
if (process.env.GH_TOKEN) {
|
|
578
|
+
return process.env.GH_TOKEN;
|
|
579
|
+
}
|
|
580
|
+
// Try to get token from gh CLI
|
|
581
|
+
try {
|
|
582
|
+
const token = execSync('gh auth token', { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
583
|
+
if (token) {
|
|
584
|
+
return token;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
// gh auth token failed - user not logged in
|
|
589
|
+
}
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Check if GitHub token is available.
|
|
594
|
+
* Returns true if token is available via env vars or gh CLI.
|
|
595
|
+
*/
|
|
596
|
+
export function isGitHubTokenAvailable() {
|
|
597
|
+
return getGitHubToken() !== null;
|
|
598
|
+
}
|
|
599
|
+
// =============================================================================
|
|
350
600
|
// Docker Status Check
|
|
351
601
|
// =============================================================================
|
|
352
602
|
/**
|
|
@@ -372,8 +622,314 @@ export function isDockerRunning() {
|
|
|
372
622
|
}
|
|
373
623
|
return false;
|
|
374
624
|
}
|
|
625
|
+
/**
|
|
626
|
+
* Check if the devcontainer CLI is installed.
|
|
627
|
+
* Returns true if the CLI is available, false otherwise.
|
|
628
|
+
* @deprecated No longer required - we use raw Docker commands now
|
|
629
|
+
*/
|
|
630
|
+
export function isDevcontainerCliInstalled() {
|
|
631
|
+
// Always return true since we no longer require devcontainer CLI
|
|
632
|
+
// We use raw Docker commands instead
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
// =============================================================================
|
|
636
|
+
// Docker Container Management (Raw Docker, no devcontainer CLI)
|
|
637
|
+
// =============================================================================
|
|
638
|
+
/**
|
|
639
|
+
* Get the container name for an agent.
|
|
640
|
+
* Format: prlt-agent-{agentName}
|
|
641
|
+
*/
|
|
642
|
+
export function getAgentContainerName(agentName) {
|
|
643
|
+
// Sanitize agent name for Docker container naming (alphanumeric, dash, underscore only)
|
|
644
|
+
const sanitized = agentName.replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
645
|
+
return `prlt-agent-${sanitized}`;
|
|
646
|
+
}
|
|
647
|
+
// Alias for internal use
|
|
648
|
+
const getContainerName = getAgentContainerName;
|
|
649
|
+
/**
|
|
650
|
+
* Get the image name for an agent.
|
|
651
|
+
* Format: prlt-agent-{agentName}:latest
|
|
652
|
+
*/
|
|
653
|
+
function getImageName(agentName) {
|
|
654
|
+
const sanitized = agentName.replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
655
|
+
return `prlt-agent-${sanitized}:latest`;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Check if a Docker container exists (running or stopped).
|
|
659
|
+
*/
|
|
660
|
+
export function containerExists(containerName) {
|
|
661
|
+
try {
|
|
662
|
+
execSync(`docker container inspect ${containerName}`, { stdio: 'pipe' });
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Check if a Docker container is running.
|
|
671
|
+
*/
|
|
672
|
+
export function isContainerRunning(containerName) {
|
|
673
|
+
try {
|
|
674
|
+
const status = execSync(`docker container inspect -f '{{.State.Running}}' ${containerName}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
675
|
+
return status === 'true';
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Get the container ID for a running container.
|
|
683
|
+
*/
|
|
684
|
+
export function getContainerId(containerName) {
|
|
685
|
+
try {
|
|
686
|
+
const containerId = execSync(`docker container inspect -f '{{.Id}}' ${containerName}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
687
|
+
return containerId ? containerId.substring(0, 12) : null;
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Build Docker image for an agent from its Dockerfile.
|
|
695
|
+
*/
|
|
696
|
+
function buildDockerImage(agentDir, imageName, buildArgs = {}) {
|
|
697
|
+
const dockerfilePath = path.join(agentDir, '.devcontainer', 'Dockerfile');
|
|
698
|
+
if (!fs.existsSync(dockerfilePath)) {
|
|
699
|
+
console.debug(`[runners:docker] Dockerfile not found at ${dockerfilePath}`);
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
// Build --build-arg flags
|
|
704
|
+
const buildArgFlags = Object.entries(buildArgs)
|
|
705
|
+
.map(([key, value]) => `--build-arg ${key}="${value}"`)
|
|
706
|
+
.join(' ');
|
|
707
|
+
const buildCmd = `docker build -t ${imageName} -f "${dockerfilePath}" ${buildArgFlags} "${path.join(agentDir, '.devcontainer')}"`;
|
|
708
|
+
console.debug(`[runners:docker] Building image: ${buildCmd}`);
|
|
709
|
+
execSync(buildCmd, { stdio: 'pipe' });
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
catch (error) {
|
|
713
|
+
console.debug(`[runners:docker] Failed to build image:`, error);
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Check if a Docker image exists.
|
|
719
|
+
*/
|
|
720
|
+
function imageExists(imageName) {
|
|
721
|
+
try {
|
|
722
|
+
execSync(`docker image inspect ${imageName}`, { stdio: 'pipe' });
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Create and start a Docker container for an agent.
|
|
731
|
+
* Uses raw Docker commands instead of devcontainer CLI.
|
|
732
|
+
*/
|
|
733
|
+
function createDockerContainer(context, containerName, imageName, config) {
|
|
734
|
+
// Build mount flags
|
|
735
|
+
// KEY: Use a named Docker volume for Claude credentials - this is how devcontainer.json
|
|
736
|
+
// was handling it. The volume persists across containers, so login once = logged in everywhere.
|
|
737
|
+
// This avoids corruption from concurrent writes to host filesystem.
|
|
738
|
+
const mounts = [
|
|
739
|
+
// Agent workspace
|
|
740
|
+
`-v "${context.agentDir}:/workspace"`,
|
|
741
|
+
// HQ .proletariat directory (for database access)
|
|
742
|
+
...(context.hqPath ? [`-v "${context.hqPath}/.proletariat:/hq/.proletariat"`] : []),
|
|
743
|
+
// PMO path
|
|
744
|
+
...(context.pmoPath ? [`-v "${context.pmoPath}:/hq/pmo"`] : []),
|
|
745
|
+
// Claude credentials - shared named volume (login once, all containers share)
|
|
746
|
+
`-v "claude-credentials:/home/node/.claude"`,
|
|
747
|
+
];
|
|
748
|
+
// Build environment flags
|
|
749
|
+
const envVars = [
|
|
750
|
+
`-e DEVCONTAINER=true`,
|
|
751
|
+
`-e PRLT_HQ_PATH=/hq`,
|
|
752
|
+
`-e PRLT_AGENT_NAME="${context.agentName}"`,
|
|
753
|
+
`-e PRLT_HOST_PATH="${context.agentDir}"`,
|
|
754
|
+
...(process.env.ANTHROPIC_API_KEY ? [`-e ANTHROPIC_API_KEY="${process.env.ANTHROPIC_API_KEY}"`] : []),
|
|
755
|
+
...(process.env.GITHUB_TOKEN ? [`-e GITHUB_TOKEN="${process.env.GITHUB_TOKEN}"`] : []),
|
|
756
|
+
...(process.env.GH_TOKEN ? [`-e GH_TOKEN="${process.env.GH_TOKEN}"`] : []),
|
|
757
|
+
// NOTE: Do NOT pass CLAUDE_CODE_OAUTH_TOKEN - it overrides credentials file
|
|
758
|
+
// and setup-token generates invalid tokens. Use "prlt agent auth" instead.
|
|
759
|
+
];
|
|
760
|
+
// Resource limits
|
|
761
|
+
const resourceFlags = [
|
|
762
|
+
`--memory=${config.devcontainer.memory}`,
|
|
763
|
+
`--cpus=${config.devcontainer.cpus}`,
|
|
764
|
+
];
|
|
765
|
+
// Security flags - these provide the sandboxing
|
|
766
|
+
const securityFlags = [
|
|
767
|
+
'--cap-add=NET_ADMIN', // For firewall setup
|
|
768
|
+
'--cap-add=NET_RAW', // For firewall setup
|
|
769
|
+
// Note: After firewall is set up, the container is network-restricted
|
|
770
|
+
];
|
|
771
|
+
try {
|
|
772
|
+
const createCmd = [
|
|
773
|
+
'docker run -d',
|
|
774
|
+
`--name ${containerName}`,
|
|
775
|
+
'--user node',
|
|
776
|
+
'-w /workspace',
|
|
777
|
+
...mounts,
|
|
778
|
+
...envVars,
|
|
779
|
+
...resourceFlags,
|
|
780
|
+
...securityFlags,
|
|
781
|
+
imageName,
|
|
782
|
+
'sleep infinity', // Keep container running
|
|
783
|
+
].join(' ');
|
|
784
|
+
console.debug(`[runners:docker] Creating container: ${createCmd}`);
|
|
785
|
+
execSync(createCmd, { stdio: 'pipe' });
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
console.debug(`[runners:docker] Failed to create container:`, error);
|
|
790
|
+
return false;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Run the post-start setup commands in a container.
|
|
795
|
+
* This includes firewall initialization, prlt setup, and Claude settings.
|
|
796
|
+
* @param containerId - Docker container ID
|
|
797
|
+
* @param sandboxed - Whether running in safe mode (true) or danger mode (false)
|
|
798
|
+
*/
|
|
799
|
+
function runContainerSetup(containerId, sandboxed = true) {
|
|
800
|
+
try {
|
|
801
|
+
// Run firewall init (requires sudo since we're running as node user)
|
|
802
|
+
execSync(`docker exec ${containerId} sudo /usr/local/bin/init-firewall.sh`, { stdio: 'pipe' });
|
|
803
|
+
// Run prlt setup
|
|
804
|
+
execSync(`docker exec ${containerId} /usr/local/bin/setup-prlt.sh`, { stdio: 'pipe' });
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
console.debug(`[runners:docker] Container setup scripts failed:`, error);
|
|
808
|
+
// Continue - setup might partially work
|
|
809
|
+
}
|
|
810
|
+
// Copy Claude settings file (.claude.json) from host to container
|
|
811
|
+
// This is needed for Claude Code to recognize settings and bypass prompts
|
|
812
|
+
// Note: Auth tokens are in the claude-credentials volume at /home/node/.claude/.credentials.json
|
|
813
|
+
// But settings (.claude.json) need to be at /home/node/.claude.json (outside the .claude dir)
|
|
814
|
+
try {
|
|
815
|
+
const hostClaudeJson = path.join(os.homedir(), '.claude.json');
|
|
816
|
+
let settings = {};
|
|
817
|
+
if (fs.existsSync(hostClaudeJson)) {
|
|
818
|
+
// Read host file content as base
|
|
819
|
+
const content = fs.readFileSync(hostClaudeJson, 'utf-8');
|
|
820
|
+
try {
|
|
821
|
+
settings = JSON.parse(content);
|
|
822
|
+
}
|
|
823
|
+
catch {
|
|
824
|
+
console.debug('[runners:docker] Failed to parse host .claude.json, using empty settings');
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
// Only set bypassPermissionsModeAccepted when user chose danger mode (!sandboxed)
|
|
828
|
+
// This doesn't modify the host file - only the container copy
|
|
829
|
+
if (!sandboxed) {
|
|
830
|
+
settings.bypassPermissionsModeAccepted = true;
|
|
831
|
+
}
|
|
832
|
+
// Skip first-run onboarding (theme picker, tips, etc.) for automated agents
|
|
833
|
+
// These flags indicate Claude Code has been run before
|
|
834
|
+
settings.numStartups = settings.numStartups || 1;
|
|
835
|
+
settings.hasCompletedOnboarding = true;
|
|
836
|
+
settings.theme = settings.theme || 'dark';
|
|
837
|
+
// Ensure tipsHistory exists to prevent tip prompts
|
|
838
|
+
if (!settings.tipsHistory || typeof settings.tipsHistory !== 'object') {
|
|
839
|
+
settings.tipsHistory = {};
|
|
840
|
+
}
|
|
841
|
+
const tips = settings.tipsHistory;
|
|
842
|
+
tips['new-user-warmup'] = tips['new-user-warmup'] || 1;
|
|
843
|
+
// Base64 encode to avoid shell escaping issues
|
|
844
|
+
const base64Content = Buffer.from(JSON.stringify(settings)).toString('base64');
|
|
845
|
+
// Write to container at /home/node/.claude.json
|
|
846
|
+
execSync(`docker exec ${containerId} bash -c 'echo "${base64Content}" | base64 -d > /home/node/.claude.json'`, { stdio: 'pipe' });
|
|
847
|
+
console.debug(`[runners:docker] Copied .claude.json settings to container (bypassPermissionsModeAccepted=${!sandboxed})`);
|
|
848
|
+
}
|
|
849
|
+
catch (error) {
|
|
850
|
+
console.debug('[runners:docker] Failed to copy .claude.json to container:', error);
|
|
851
|
+
// Non-fatal - Claude will just prompt for settings
|
|
852
|
+
}
|
|
853
|
+
// NOTE: Auth credentials come from the claude-credentials volume.
|
|
854
|
+
// Run "prlt agent auth" to set up authentication (one-time).
|
|
855
|
+
// Do NOT sync CLAUDE_CODE_OAUTH_TOKEN env var - it causes issues
|
|
856
|
+
// (setup-token generates invalid tokens, and env var overrides valid credentials file).
|
|
857
|
+
return true;
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Ensure a Docker container is running for the agent.
|
|
861
|
+
* Builds image and creates container if needed.
|
|
862
|
+
* Returns the container ID if successful, null otherwise.
|
|
863
|
+
*/
|
|
864
|
+
function ensureDockerContainer(context, config) {
|
|
865
|
+
const containerName = getContainerName(context.agentName);
|
|
866
|
+
const imageName = getImageName(context.agentName);
|
|
867
|
+
// Always create fresh container to ensure mounts are up-to-date
|
|
868
|
+
// TODO: Revisit container reuse strategy - for now, fresh containers ensure
|
|
869
|
+
// correct volume mounts (especially claude-credentials) are applied
|
|
870
|
+
if (containerExists(containerName)) {
|
|
871
|
+
console.debug(`[runners:docker] Removing existing container ${containerName} to create fresh one`);
|
|
872
|
+
try {
|
|
873
|
+
execSync(`docker rm -f ${containerName}`, { stdio: 'pipe' });
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
// Ignore removal errors
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
// Build image if it doesn't exist
|
|
880
|
+
if (!imageExists(imageName)) {
|
|
881
|
+
console.debug(`[runners:docker] Building image ${imageName}`);
|
|
882
|
+
const buildArgs = {
|
|
883
|
+
TZ: 'America/Los_Angeles',
|
|
884
|
+
PRLT_REGISTRY: 'npm',
|
|
885
|
+
PRLT_VERSION: 'latest',
|
|
886
|
+
};
|
|
887
|
+
if (!buildDockerImage(context.agentDir, imageName, buildArgs)) {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
// Create and start container
|
|
892
|
+
console.debug(`[runners:docker] Creating container ${containerName}`);
|
|
893
|
+
if (!createDockerContainer(context, containerName, imageName, config)) {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
const containerId = getContainerId(containerName);
|
|
897
|
+
if (!containerId) {
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
// Run post-start setup (firewall, prlt, Claude settings)
|
|
901
|
+
// Pass sandboxed config to determine whether to set bypassPermissionsModeAccepted
|
|
902
|
+
console.debug(`[runners:docker] Running container setup (sandboxed=${config.sandboxed})`);
|
|
903
|
+
if (!runContainerSetup(containerId, config.sandboxed)) {
|
|
904
|
+
console.debug(`[runners:docker] Setup failed, but continuing...`);
|
|
905
|
+
// Don't fail completely - setup might partially work
|
|
906
|
+
}
|
|
907
|
+
// NOTE: Claude credentials are copied to workspace before container creation
|
|
908
|
+
// (see copyClaudeCredentials call in runDevcontainer)
|
|
909
|
+
return containerId;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Copy Claude Code credentials (~/.claude.json) into the agent directory.
|
|
913
|
+
* This makes the subscription credentials available inside the devcontainer
|
|
914
|
+
* since the agent directory is mounted at /workspace.
|
|
915
|
+
*
|
|
916
|
+
* This was the original working approach before the raw Docker refactor.
|
|
917
|
+
*/
|
|
918
|
+
function copyClaudeCredentials(agentDir) {
|
|
919
|
+
const sourceFile = path.join(os.homedir(), '.claude.json');
|
|
920
|
+
const destFile = path.join(agentDir, '.claude.json');
|
|
921
|
+
if (fs.existsSync(sourceFile)) {
|
|
922
|
+
try {
|
|
923
|
+
fs.copyFileSync(sourceFile, destFile);
|
|
924
|
+
console.debug('[runners:credentials] Copied .claude.json to workspace');
|
|
925
|
+
}
|
|
926
|
+
catch (err) {
|
|
927
|
+
console.debug('[runners:credentials] Failed to copy .claude.json:', err);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
375
931
|
// =============================================================================
|
|
376
|
-
// Devcontainer Runner
|
|
932
|
+
// Devcontainer Runner (now uses raw Docker)
|
|
377
933
|
// =============================================================================
|
|
378
934
|
/**
|
|
379
935
|
* Clean up old prompt files from the worktree.
|
|
@@ -425,31 +981,9 @@ function writePromptFile(context) {
|
|
|
425
981
|
}
|
|
426
982
|
/**
|
|
427
983
|
* Build the command to run Claude inside the container.
|
|
428
|
-
* Uses
|
|
984
|
+
* Uses docker exec for direct container access.
|
|
429
985
|
* Uses a prompt file to avoid shell escaping issues.
|
|
430
986
|
*/
|
|
431
|
-
/**
|
|
432
|
-
* Get the container ID for a devcontainer workspace.
|
|
433
|
-
*/
|
|
434
|
-
function getDevcontainerContainerId(agentDir) {
|
|
435
|
-
try {
|
|
436
|
-
// devcontainer up outputs JSON with container ID
|
|
437
|
-
const result = execSync(`devcontainer up --workspace-folder "${agentDir}" 2>/dev/null | tail -1`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
438
|
-
const json = JSON.parse(result.trim());
|
|
439
|
-
return json.containerId || null;
|
|
440
|
-
}
|
|
441
|
-
catch (err) {
|
|
442
|
-
console.debug('[runners:devcontainer] devcontainer up failed, trying docker ps fallback:', err);
|
|
443
|
-
try {
|
|
444
|
-
const containerId = execSync(`docker ps -q --filter "label=devcontainer.local_folder=${agentDir}"`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
445
|
-
return containerId || null;
|
|
446
|
-
}
|
|
447
|
-
catch (fallbackErr) {
|
|
448
|
-
console.debug('[runners:devcontainer] docker ps fallback also failed:', fallbackErr);
|
|
449
|
-
return null;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
987
|
function buildDevcontainerCommand(context, executor, promptFile, containerId, outputMode = 'interactive', sandboxed = true, displayMode = 'terminal') {
|
|
454
988
|
// Get base command (just 'claude' for claude-code)
|
|
455
989
|
let baseCmd;
|
|
@@ -475,69 +1009,39 @@ function buildDevcontainerCommand(context, executor, promptFile, containerId, ou
|
|
|
475
1009
|
const printFlag = outputMode === 'print' ? '-p ' : '';
|
|
476
1010
|
// sandboxed=true means safe mode (no --dangerously-skip-permissions)
|
|
477
1011
|
// sandboxed=false means danger mode (use --dangerously-skip-permissions)
|
|
1012
|
+
// --permission-mode bypassPermissions: skips the "trust this folder" dialog
|
|
1013
|
+
const bypassTrustFlag = '--permission-mode bypassPermissions ';
|
|
478
1014
|
const permissionsFlag = !sandboxed ? '--dangerously-skip-permissions ' : '';
|
|
479
1015
|
// Build the claude command
|
|
480
|
-
const claudeCmd = `${cdCmd}${baseCmd} ${permissionsFlag}${printFlag}"$(cat ${promptFile})" && rm -f ${promptFile}`;
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
return `docker exec ${ttyFlags}${containerId} bash -c '${claudeCmd}'`;
|
|
488
|
-
}
|
|
489
|
-
// Fallback to devcontainer exec (no streaming, but works)
|
|
490
|
-
return `devcontainer exec --workspace-folder "${context.agentDir}" bash -c '${claudeCmd}'`;
|
|
1016
|
+
const claudeCmd = `${cdCmd}${baseCmd} ${bypassTrustFlag}${permissionsFlag}${printFlag}"$(cat ${promptFile})" && rm -f ${promptFile}`;
|
|
1017
|
+
// Use docker exec for running commands in the container
|
|
1018
|
+
// Use -it flags only for terminal/foreground modes where a TTY is available
|
|
1019
|
+
// Background mode runs without a TTY, so -it flags would cause "not a TTY" error
|
|
1020
|
+
const ttyFlags = displayMode === 'background' ? '' : '-it ';
|
|
1021
|
+
// Direct mode - run claude directly (tmux setup is handled by runDevcontainerInTmux)
|
|
1022
|
+
return `docker exec ${ttyFlags}${containerId} bash -c '${claudeCmd}'`;
|
|
491
1023
|
}
|
|
492
1024
|
/**
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
*/
|
|
497
|
-
function copyClaudeCredentials(agentDir) {
|
|
498
|
-
const sourceFile = path.join(os.homedir(), '.claude.json');
|
|
499
|
-
const destFile = path.join(agentDir, '.claude.json');
|
|
500
|
-
if (fs.existsSync(sourceFile)) {
|
|
501
|
-
try {
|
|
502
|
-
fs.copyFileSync(sourceFile, destFile);
|
|
503
|
-
}
|
|
504
|
-
catch (err) {
|
|
505
|
-
console.debug('[runners:credentials] Failed to copy .claude.json:', err);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
/**
|
|
510
|
-
* Run command inside a devcontainer.
|
|
511
|
-
* Uses the devcontainer CLI to start/exec in a VS Code devcontainer.
|
|
512
|
-
* Provides filesystem isolation - agent can only access mounted worktrees.
|
|
1025
|
+
* Run command inside a Docker container.
|
|
1026
|
+
* Uses raw Docker commands for filesystem isolation - no devcontainer CLI required.
|
|
1027
|
+
* Agent can only access mounted worktrees and configured paths.
|
|
513
1028
|
*
|
|
514
1029
|
* @param displayMode - How to display output (terminal, foreground, background, tmux)
|
|
515
1030
|
* @param sessionManager - How to manage the session inside the container (tmux, direct)
|
|
516
1031
|
*/
|
|
517
|
-
export async function runDevcontainer(context, executor, config, displayMode = 'terminal', sessionManager = '
|
|
518
|
-
|
|
519
|
-
//
|
|
1032
|
+
export async function runDevcontainer(context, executor, config, displayMode = 'terminal', sessionManager = 'tmux' // Default to tmux for session persistence
|
|
1033
|
+
) {
|
|
1034
|
+
// Docker config is in the agent directory (still uses .devcontainer for Dockerfile)
|
|
520
1035
|
const devcontainerPath = path.join(context.agentDir, '.devcontainer');
|
|
521
|
-
const
|
|
522
|
-
// Check if
|
|
523
|
-
if (!fs.existsSync(
|
|
1036
|
+
const dockerfile = path.join(devcontainerPath, 'Dockerfile');
|
|
1037
|
+
// Check if Dockerfile exists
|
|
1038
|
+
if (!fs.existsSync(dockerfile)) {
|
|
524
1039
|
return {
|
|
525
1040
|
success: false,
|
|
526
|
-
error: `No
|
|
1041
|
+
error: `No Dockerfile found at ${devcontainerPath}. Run 'prlt agent add' to set up the agent with Docker config.`,
|
|
527
1042
|
};
|
|
528
1043
|
}
|
|
529
1044
|
try {
|
|
530
|
-
// Check devcontainer CLI is installed
|
|
531
|
-
try {
|
|
532
|
-
execSync('which devcontainer', { stdio: 'pipe' });
|
|
533
|
-
}
|
|
534
|
-
catch (err) {
|
|
535
|
-
console.debug('[runners:devcontainer] devcontainer CLI not found:', err);
|
|
536
|
-
return {
|
|
537
|
-
success: false,
|
|
538
|
-
error: 'devcontainer CLI not found. Install with: npm install -g @devcontainers/cli',
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
1045
|
// Check if Docker is running
|
|
542
1046
|
if (!isDockerRunning()) {
|
|
543
1047
|
return {
|
|
@@ -545,65 +1049,51 @@ export async function runDevcontainer(context, executor, config, displayMode = '
|
|
|
545
1049
|
error: 'Docker is not running. Please start Docker Desktop and try again.',
|
|
546
1050
|
};
|
|
547
1051
|
}
|
|
548
|
-
// Copy Claude credentials into agent directory so container can access them
|
|
549
|
-
copyClaudeCredentials(context.agentDir);
|
|
550
|
-
// Set environment variables for devcontainer mounts
|
|
551
|
-
// PRLT_HQ_PATH: allows agent to access the HQ database and run `prlt ticket complete`
|
|
552
|
-
// PRLT_PMO_PATH: allows agent to access the PMO (can be anywhere, e.g., /hq/repos/myrepo/pmo)
|
|
553
|
-
// PRLT_REPO_PATH: mounts the entire proletariat repo into the container (until prlt is on npm)
|
|
554
|
-
const env = { ...process.env };
|
|
555
|
-
if (context.hqPath) {
|
|
556
|
-
env.PRLT_HQ_PATH = context.hqPath;
|
|
557
|
-
}
|
|
558
|
-
if (context.pmoPath) {
|
|
559
|
-
env.PRLT_PMO_PATH = context.pmoPath;
|
|
560
|
-
}
|
|
561
1052
|
// Ensure GitHub token is available for git push operations
|
|
562
1053
|
// Try to get token from gh CLI if not already in environment
|
|
563
|
-
if (!env.GITHUB_TOKEN && !env.GH_TOKEN) {
|
|
1054
|
+
if (!process.env.GITHUB_TOKEN && !process.env.GH_TOKEN) {
|
|
564
1055
|
try {
|
|
565
1056
|
const token = execSync('gh auth token', { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
566
1057
|
if (token) {
|
|
567
|
-
env.GITHUB_TOKEN = token;
|
|
568
|
-
env.GH_TOKEN = token;
|
|
1058
|
+
process.env.GITHUB_TOKEN = token;
|
|
1059
|
+
process.env.GH_TOKEN = token;
|
|
569
1060
|
}
|
|
570
1061
|
}
|
|
571
1062
|
catch (err) {
|
|
572
|
-
console.debug('[runners:
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
// Set repo path to the proletariat monorepo (auto-detect from current CLI location)
|
|
576
|
-
// We mount the entire repo so node_modules resolution works correctly
|
|
577
|
-
if (!env.PRLT_REPO_PATH) {
|
|
578
|
-
// Get the directory where this CLI is running from (apps/cli)
|
|
579
|
-
const cliDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..', '..');
|
|
580
|
-
// Go up to the monorepo root (repos/proletariat)
|
|
581
|
-
const repoDir = path.resolve(cliDir, '..', '..');
|
|
582
|
-
if (fs.existsSync(path.join(repoDir, 'apps', 'cli', 'bin', 'run.js'))) {
|
|
583
|
-
env.PRLT_REPO_PATH = repoDir;
|
|
1063
|
+
console.debug('[runners:docker] gh auth token failed:', err);
|
|
584
1064
|
}
|
|
585
1065
|
}
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
catch (error) {
|
|
1066
|
+
// Copy Claude credentials into agent directory so container can access them
|
|
1067
|
+
// This was the original working approach - credentials at /workspace/.claude.json
|
|
1068
|
+
copyClaudeCredentials(context.agentDir);
|
|
1069
|
+
// Start or reuse container using raw Docker commands
|
|
1070
|
+
// No devcontainer CLI required!
|
|
1071
|
+
const containerId = ensureDockerContainer(context, config);
|
|
1072
|
+
if (!containerId) {
|
|
595
1073
|
return {
|
|
596
1074
|
success: false,
|
|
597
|
-
error:
|
|
1075
|
+
error: 'Failed to start Docker container. Check Docker logs for details.',
|
|
598
1076
|
};
|
|
599
1077
|
}
|
|
600
1078
|
// Write prompt to file in worktree (accessible by container)
|
|
601
1079
|
const { hostPath: promptHostPath, containerPath: promptFile } = writePromptFile(context);
|
|
602
|
-
//
|
|
603
|
-
|
|
604
|
-
|
|
1080
|
+
// Inject fresh GitHub token into container (containers may be reused with stale/empty tokens)
|
|
1081
|
+
// This ensures git push works even if the container was created before token was available
|
|
1082
|
+
const githubToken = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
1083
|
+
if (containerId && githubToken) {
|
|
1084
|
+
try {
|
|
1085
|
+
// Write token to file and configure git credential helper
|
|
1086
|
+
execSync(`docker exec ${containerId} bash -c 'echo "${githubToken}" > /home/node/.github-token && chmod 600 /home/node/.github-token && git config --global credential.helper "!f() { echo \\"username=x-access-token\\"; echo \\"password=\\$(cat /home/node/.github-token)\\"; }; f" && git config --global url."https://github.com/".insteadOf "git@github.com:"'`, {
|
|
1087
|
+
stdio: 'pipe',
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
// Non-fatal - token injection failed but execution can continue
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
// Build the docker exec command (just runs claude directly)
|
|
605
1095
|
// tmux session setup is handled by runDevcontainerInTmux, not buildDevcontainerCommand
|
|
606
|
-
const devcontainerCmd = buildDevcontainerCommand(context, executor, promptFile, containerId
|
|
1096
|
+
const devcontainerCmd = buildDevcontainerCommand(context, executor, promptFile, containerId, config.outputMode, config.sandboxed, displayMode);
|
|
607
1097
|
// Execute based on display mode
|
|
608
1098
|
// When sessionManager is 'tmux', always use tmux inside container for session persistence
|
|
609
1099
|
// (allows reattach via `prlt session attach` even for background mode)
|
|
@@ -650,7 +1140,7 @@ export async function runDevcontainer(context, executor, config, displayMode = '
|
|
|
650
1140
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
651
1141
|
// Check if tmux session exists inside the container
|
|
652
1142
|
try {
|
|
653
|
-
|
|
1143
|
+
execSync(`docker exec ${containerId} tmux has-session -t "${sessionId}" 2>&1`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
654
1144
|
// Session exists - success
|
|
655
1145
|
}
|
|
656
1146
|
catch (err) {
|
|
@@ -712,21 +1202,66 @@ rm -f "${scriptPath}"
|
|
|
712
1202
|
exec $SHELL
|
|
713
1203
|
`;
|
|
714
1204
|
fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
|
|
1205
|
+
// Check if we should open in background (don't steal focus)
|
|
1206
|
+
const openInBackground = config.terminal.openInBackground ?? true;
|
|
715
1207
|
try {
|
|
716
1208
|
switch (terminalApp) {
|
|
717
1209
|
case 'iTerm':
|
|
718
1210
|
// Run script file directly - iTerm will execute it with proper TTY
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
end tell
|
|
1211
|
+
// When openInBackground is true, save frontmost app and restore after
|
|
1212
|
+
if (openInBackground) {
|
|
1213
|
+
execSync(`osascript -e '
|
|
1214
|
+
-- Save the currently active application and window
|
|
1215
|
+
tell application "System Events"
|
|
1216
|
+
set frontApp to name of first application process whose frontmost is true
|
|
1217
|
+
set frontAppBundle to bundle identifier of first application process whose frontmost is true
|
|
727
1218
|
end tell
|
|
728
|
-
|
|
729
|
-
|
|
1219
|
+
|
|
1220
|
+
tell application "iTerm"
|
|
1221
|
+
if (count of windows) = 0 then
|
|
1222
|
+
create window with default profile
|
|
1223
|
+
tell current session of current window
|
|
1224
|
+
write text "${scriptPath}"
|
|
1225
|
+
end tell
|
|
1226
|
+
else
|
|
1227
|
+
tell current window
|
|
1228
|
+
set newTab to (create tab with default profile)
|
|
1229
|
+
tell current session of newTab
|
|
1230
|
+
write text "${scriptPath}"
|
|
1231
|
+
end tell
|
|
1232
|
+
end tell
|
|
1233
|
+
end if
|
|
1234
|
+
end tell
|
|
1235
|
+
|
|
1236
|
+
-- Restore focus to the original application
|
|
1237
|
+
delay 0.2
|
|
1238
|
+
tell application "System Events"
|
|
1239
|
+
set frontmost of process frontApp to true
|
|
1240
|
+
end tell
|
|
1241
|
+
delay 0.1
|
|
1242
|
+
do shell script "open -b " & quoted form of frontAppBundle
|
|
1243
|
+
'`);
|
|
1244
|
+
}
|
|
1245
|
+
else {
|
|
1246
|
+
execSync(`osascript -e '
|
|
1247
|
+
tell application "iTerm"
|
|
1248
|
+
activate
|
|
1249
|
+
if (count of windows) = 0 then
|
|
1250
|
+
create window with default profile
|
|
1251
|
+
tell current session of current window
|
|
1252
|
+
write text "${scriptPath}"
|
|
1253
|
+
end tell
|
|
1254
|
+
else
|
|
1255
|
+
tell current window
|
|
1256
|
+
set newTab to (create tab with default profile)
|
|
1257
|
+
tell current session of newTab
|
|
1258
|
+
write text "${scriptPath}"
|
|
1259
|
+
end tell
|
|
1260
|
+
end tell
|
|
1261
|
+
end if
|
|
1262
|
+
end tell
|
|
1263
|
+
'`);
|
|
1264
|
+
}
|
|
730
1265
|
break;
|
|
731
1266
|
case 'Ghostty':
|
|
732
1267
|
// Use source to preserve TTY for docker exec
|
|
@@ -771,18 +1306,29 @@ exec $SHELL
|
|
|
771
1306
|
case 'Terminal':
|
|
772
1307
|
default:
|
|
773
1308
|
// Use source to preserve TTY for docker exec
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
tell application "
|
|
778
|
-
|
|
779
|
-
|
|
1309
|
+
if (openInBackground) {
|
|
1310
|
+
// Open in background: use 'do script' which creates a new window without activating
|
|
1311
|
+
execSync(`osascript -e '
|
|
1312
|
+
tell application "Terminal"
|
|
1313
|
+
do script "source ${scriptPath}"
|
|
1314
|
+
end tell
|
|
1315
|
+
'`);
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
// Bring to front: use traditional Cmd+T for new tab
|
|
1319
|
+
execSync(`osascript -e '
|
|
1320
|
+
tell application "Terminal"
|
|
1321
|
+
activate
|
|
1322
|
+
tell application "System Events"
|
|
1323
|
+
tell process "Terminal"
|
|
1324
|
+
keystroke "t" using command down
|
|
1325
|
+
end tell
|
|
780
1326
|
end tell
|
|
1327
|
+
delay 0.3
|
|
1328
|
+
do script "source ${scriptPath}" in front window
|
|
781
1329
|
end tell
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
end tell
|
|
785
|
-
'`);
|
|
1330
|
+
'`);
|
|
1331
|
+
}
|
|
786
1332
|
break;
|
|
787
1333
|
}
|
|
788
1334
|
return {
|
|
@@ -836,6 +1382,10 @@ async function runDevcontainerInTmux(context, devcontainerCmd, config, displayMo
|
|
|
836
1382
|
// Session name: {ticketId}-{action} (e.g., TKT-347-implement)
|
|
837
1383
|
const sessionName = buildTmuxWindowName(context);
|
|
838
1384
|
const windowTitle = buildWindowTitle(context);
|
|
1385
|
+
// Check if we should use iTerm control mode (-CC)
|
|
1386
|
+
// When using -CC, iTerm handles scrolling/selection natively, so we DON'T set mouse on
|
|
1387
|
+
const terminalApp = config.terminal.app;
|
|
1388
|
+
const useControlMode = shouldUseControlMode(terminalApp, config.tmux.controlMode);
|
|
839
1389
|
try {
|
|
840
1390
|
// Get container ID - prefer passed value, fallback to extracting from command
|
|
841
1391
|
// The devcontainerCmd is like: docker exec [-it] <containerId> bash -c '...'
|
|
@@ -870,7 +1420,14 @@ async function runDevcontainerInTmux(context, devcontainerCmd, config, displayMo
|
|
|
870
1420
|
const cmdMatch = devcontainerCmd.match(/bash -c '(.+)'$/);
|
|
871
1421
|
const claudeCmd = cmdMatch ? cmdMatch[1] : devcontainerCmd;
|
|
872
1422
|
// Create a script inside the container that runs claude and keeps shell open
|
|
1423
|
+
// TERM must be set for Claude's TUI to render properly
|
|
1424
|
+
// Unset DEVCONTAINER and CI to prevent Claude from detecting container/CI environment
|
|
1425
|
+
// which might cause it to suppress TUI output
|
|
873
1426
|
const tmuxScript = `#!/bin/bash
|
|
1427
|
+
export TERM=xterm-256color
|
|
1428
|
+
export COLORTERM=truecolor
|
|
1429
|
+
unset DEVCONTAINER
|
|
1430
|
+
unset CI
|
|
874
1431
|
echo "🚀 Starting: ${sessionName}"
|
|
875
1432
|
echo ""
|
|
876
1433
|
${claudeCmd}
|
|
@@ -881,18 +1438,46 @@ exec bash
|
|
|
881
1438
|
const base64Script = Buffer.from(tmuxScript).toString('base64');
|
|
882
1439
|
const scriptPath = `/tmp/prlt-${sessionName}.sh`;
|
|
883
1440
|
// Write script and start tmux session inside container
|
|
1441
|
+
// IMPORTANT: We create the session with bash first, then send keys to run the script.
|
|
1442
|
+
// This ensures bash is running interactively (required for Claude's TUI to render).
|
|
1443
|
+
// If we pass the script as the session command, bash runs non-interactively and Claude won't show TUI.
|
|
884
1444
|
// -n sets the window name (shows in iTerm tab title with -CC mode)
|
|
885
1445
|
// sessionName is already ticket-action-agent format
|
|
886
|
-
//
|
|
1446
|
+
// Only enable mouse mode if NOT using control mode (control mode lets iTerm handle mouse natively)
|
|
887
1447
|
// set-titles on + set-titles-string: makes tmux set terminal title to window name
|
|
888
|
-
const
|
|
1448
|
+
const mouseOption = buildTmuxMouseOption(useControlMode);
|
|
1449
|
+
// Step 1: Write the script to the container
|
|
1450
|
+
const writeScriptCmd = `echo ${base64Script} | base64 -d > ${scriptPath} && chmod +x ${scriptPath}`;
|
|
1451
|
+
try {
|
|
1452
|
+
execSync(`docker exec ${actualContainerId} bash -c '${writeScriptCmd}'`, { stdio: 'pipe' });
|
|
1453
|
+
}
|
|
1454
|
+
catch (error) {
|
|
1455
|
+
return {
|
|
1456
|
+
success: false,
|
|
1457
|
+
error: `Failed to write script to container: ${error instanceof Error ? error.message : error}`,
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
// Step 2: Create tmux session with bash explicitly (not default shell which may be zsh)
|
|
1461
|
+
// Using bash avoids zsh-newuser-install prompt that blocks the session
|
|
1462
|
+
const createSessionCmd = `tmux new-session -d -s "${sessionName}" -n "${sessionName}" bash${mouseOption} \\; set-option -g set-titles on \\; set-option -g set-titles-string "#{window_name}"`;
|
|
889
1463
|
try {
|
|
890
|
-
execSync(`docker exec ${actualContainerId} bash -c '${
|
|
1464
|
+
execSync(`docker exec ${actualContainerId} bash -c '${createSessionCmd}'`, { stdio: 'pipe' });
|
|
891
1465
|
}
|
|
892
1466
|
catch (error) {
|
|
893
1467
|
return {
|
|
894
1468
|
success: false,
|
|
895
|
-
error: `Failed to
|
|
1469
|
+
error: `Failed to create tmux session inside container: ${error instanceof Error ? error.message : error}`,
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
// Step 3: Send keys to run the script (this runs in the interactive bash)
|
|
1473
|
+
const sendKeysCmd = `tmux send-keys -t "${sessionName}" "source ${scriptPath}" Enter`;
|
|
1474
|
+
try {
|
|
1475
|
+
execSync(`docker exec ${actualContainerId} bash -c '${sendKeysCmd}'`, { stdio: 'pipe' });
|
|
1476
|
+
}
|
|
1477
|
+
catch (error) {
|
|
1478
|
+
return {
|
|
1479
|
+
success: false,
|
|
1480
|
+
error: `Failed to start script in tmux session: ${error instanceof Error ? error.message : error}`,
|
|
896
1481
|
};
|
|
897
1482
|
}
|
|
898
1483
|
// Step 2: Open iTerm tab that attaches directly to container's tmux
|
|
@@ -905,11 +1490,74 @@ exec bash
|
|
|
905
1490
|
sessionId: sessionName, // Container tmux session name for tracking
|
|
906
1491
|
};
|
|
907
1492
|
}
|
|
908
|
-
//
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1493
|
+
// Foreground mode: attach to container's tmux session in current terminal (blocking)
|
|
1494
|
+
if (displayMode === 'foreground') {
|
|
1495
|
+
try {
|
|
1496
|
+
// Clear screen and attach - this blocks until user detaches or claude exits
|
|
1497
|
+
// Use -CC for iTerm when control mode is enabled
|
|
1498
|
+
const fgTmuxAttach = buildTmuxAttachCommand(useControlMode, true);
|
|
1499
|
+
execSync(`clear && docker exec -it ${actualContainerId} ${fgTmuxAttach} -t "${sessionName}"`, { stdio: 'inherit' });
|
|
1500
|
+
return {
|
|
1501
|
+
success: true,
|
|
1502
|
+
containerId: actualContainerId,
|
|
1503
|
+
sessionId: sessionName,
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
catch (error) {
|
|
1507
|
+
return {
|
|
1508
|
+
success: false,
|
|
1509
|
+
error: `Failed to attach to container tmux session: ${error instanceof Error ? error.message : error}`,
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
// Use tmux -CC (control mode) for iTerm when enabled in config
|
|
1514
|
+
// -CC gives native iTerm scrolling, selection, and gesture support
|
|
1515
|
+
// Without -CC, use regular attach (relies on mouse mode for scrolling)
|
|
1516
|
+
const tmuxAttach = buildTmuxAttachCommand(useControlMode, true);
|
|
1517
|
+
const attachCmd = `docker exec -it ${actualContainerId} ${tmuxAttach} -t "${sessionName}"`;
|
|
1518
|
+
// Open terminal and run the attach command
|
|
1519
|
+
const terminalApp = config.terminal.app;
|
|
1520
|
+
// For iTerm with control mode, create a new tab and run -CC attach there
|
|
1521
|
+
// This avoids interfering with the terminal where prlt is running
|
|
1522
|
+
if (terminalApp === 'iTerm' && useControlMode) {
|
|
1523
|
+
// Configure iTerm to open tmux windows as tabs or windows based on user preference
|
|
1524
|
+
configureITermTmuxWindowMode(config.tmux.windowMode);
|
|
1525
|
+
const openInBackground = config.terminal.openInBackground ?? true;
|
|
1526
|
+
if (openInBackground) {
|
|
1527
|
+
// Open tab without stealing focus - save frontmost app and restore after
|
|
1528
|
+
execSync(`osascript -e '
|
|
1529
|
+
set frontApp to path to frontmost application as text
|
|
1530
|
+
tell application "iTerm"
|
|
1531
|
+
tell current window
|
|
1532
|
+
set newTab to (create tab with default profile)
|
|
1533
|
+
tell current session of newTab
|
|
1534
|
+
write text "docker exec -it ${actualContainerId} tmux -u -CC attach -t \\"${sessionName}\\""
|
|
1535
|
+
end tell
|
|
1536
|
+
end tell
|
|
1537
|
+
end tell
|
|
1538
|
+
tell application frontApp to activate
|
|
1539
|
+
'`);
|
|
1540
|
+
}
|
|
1541
|
+
else {
|
|
1542
|
+
execSync(`osascript -e '
|
|
1543
|
+
tell application "iTerm"
|
|
1544
|
+
activate
|
|
1545
|
+
tell current window
|
|
1546
|
+
set newTab to (create tab with default profile)
|
|
1547
|
+
tell current session of newTab
|
|
1548
|
+
write text "docker exec -it ${actualContainerId} tmux -u -CC attach -t \\"${sessionName}\\""
|
|
1549
|
+
end tell
|
|
1550
|
+
end tell
|
|
1551
|
+
end tell
|
|
1552
|
+
'`);
|
|
1553
|
+
}
|
|
1554
|
+
return {
|
|
1555
|
+
success: true,
|
|
1556
|
+
containerId: actualContainerId,
|
|
1557
|
+
sessionId: sessionName,
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
// For all other cases, create a script file and open in a new tab
|
|
913
1561
|
const baseDir = context.hqPath
|
|
914
1562
|
? path.join(context.hqPath, '.proletariat', 'scripts')
|
|
915
1563
|
: path.join(os.homedir(), '.proletariat', 'scripts');
|
|
@@ -928,32 +1576,69 @@ rm -f "${hostScriptPath}"
|
|
|
928
1576
|
exec $SHELL
|
|
929
1577
|
`;
|
|
930
1578
|
fs.writeFileSync(hostScriptPath, hostScript, { mode: 0o755 });
|
|
931
|
-
//
|
|
932
|
-
const
|
|
1579
|
+
// Check if we should open in background (don't steal focus)
|
|
1580
|
+
const openInBackground = config.terminal.openInBackground ?? true;
|
|
933
1581
|
switch (terminalApp) {
|
|
934
1582
|
case 'iTerm':
|
|
935
|
-
//
|
|
936
|
-
//
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
tell current session
|
|
1583
|
+
// Without control mode, create a new tab and attach normally
|
|
1584
|
+
// When openInBackground is true, save frontmost app and restore after
|
|
1585
|
+
if (openInBackground) {
|
|
1586
|
+
execSync(`osascript -e '
|
|
1587
|
+
-- Save the currently active application and window
|
|
1588
|
+
tell application "System Events"
|
|
1589
|
+
set frontApp to name of first application process whose frontmost is true
|
|
1590
|
+
set frontAppBundle to bundle identifier of first application process whose frontmost is true
|
|
1591
|
+
end tell
|
|
1592
|
+
|
|
1593
|
+
tell application "iTerm"
|
|
1594
|
+
if (count of windows) = 0 then
|
|
1595
|
+
create window with default profile
|
|
1596
|
+
tell current session of current window
|
|
950
1597
|
set name to "${windowTitle}"
|
|
951
1598
|
write text "${hostScriptPath}"
|
|
952
1599
|
end tell
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1600
|
+
else
|
|
1601
|
+
tell current window
|
|
1602
|
+
create tab with default profile
|
|
1603
|
+
tell current session
|
|
1604
|
+
set name to "${windowTitle}"
|
|
1605
|
+
write text "${hostScriptPath}"
|
|
1606
|
+
end tell
|
|
1607
|
+
end tell
|
|
1608
|
+
end if
|
|
1609
|
+
end tell
|
|
1610
|
+
|
|
1611
|
+
-- Restore focus to the original application
|
|
1612
|
+
delay 0.2
|
|
1613
|
+
tell application "System Events"
|
|
1614
|
+
set frontmost of process frontApp to true
|
|
1615
|
+
end tell
|
|
1616
|
+
delay 0.1
|
|
1617
|
+
do shell script "open -b " & quoted form of frontAppBundle
|
|
1618
|
+
'`);
|
|
1619
|
+
}
|
|
1620
|
+
else {
|
|
1621
|
+
execSync(`osascript -e '
|
|
1622
|
+
tell application "iTerm"
|
|
1623
|
+
activate
|
|
1624
|
+
if (count of windows) = 0 then
|
|
1625
|
+
create window with default profile
|
|
1626
|
+
tell current session of current window
|
|
1627
|
+
set name to "${windowTitle}"
|
|
1628
|
+
write text "${hostScriptPath}"
|
|
1629
|
+
end tell
|
|
1630
|
+
else
|
|
1631
|
+
tell current window
|
|
1632
|
+
create tab with default profile
|
|
1633
|
+
tell current session
|
|
1634
|
+
set name to "${windowTitle}"
|
|
1635
|
+
write text "${hostScriptPath}"
|
|
1636
|
+
end tell
|
|
1637
|
+
end tell
|
|
1638
|
+
end if
|
|
1639
|
+
end tell
|
|
1640
|
+
'`);
|
|
1641
|
+
}
|
|
957
1642
|
break;
|
|
958
1643
|
case 'Ghostty':
|
|
959
1644
|
execSync(`osascript -e '
|
|
@@ -972,18 +1657,29 @@ exec $SHELL
|
|
|
972
1657
|
break;
|
|
973
1658
|
case 'Terminal':
|
|
974
1659
|
default:
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
tell application "
|
|
979
|
-
|
|
980
|
-
|
|
1660
|
+
if (openInBackground) {
|
|
1661
|
+
// Open in background: use 'do script' which creates a new window without activating
|
|
1662
|
+
execSync(`osascript -e '
|
|
1663
|
+
tell application "Terminal"
|
|
1664
|
+
do script "${hostScriptPath}"
|
|
1665
|
+
end tell
|
|
1666
|
+
'`);
|
|
1667
|
+
}
|
|
1668
|
+
else {
|
|
1669
|
+
// Bring to front: use traditional Cmd+T for new tab
|
|
1670
|
+
execSync(`osascript -e '
|
|
1671
|
+
tell application "Terminal"
|
|
1672
|
+
activate
|
|
1673
|
+
tell application "System Events"
|
|
1674
|
+
tell process "Terminal"
|
|
1675
|
+
keystroke "t" using command down
|
|
1676
|
+
end tell
|
|
981
1677
|
end tell
|
|
1678
|
+
delay 0.3
|
|
1679
|
+
do script "${hostScriptPath}" in front window
|
|
982
1680
|
end tell
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
end tell
|
|
986
|
-
'`);
|
|
1681
|
+
'`);
|
|
1682
|
+
}
|
|
987
1683
|
break;
|
|
988
1684
|
}
|
|
989
1685
|
return {
|
|
@@ -1002,6 +1698,7 @@ exec $SHELL
|
|
|
1002
1698
|
/**
|
|
1003
1699
|
* Legacy: Run devcontainer in host-side tmux (kept for non-container modes)
|
|
1004
1700
|
*/
|
|
1701
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1005
1702
|
async function runDevcontainerInHostTmux(context, devcontainerCmd, config) {
|
|
1006
1703
|
const sessionName = config.tmux.session;
|
|
1007
1704
|
const windowName = buildTmuxWindowName(context);
|