@alanbem/dclaude 0.0.0-dev
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/LICENSE +21 -0
- package/README.md +319 -0
- package/dclaude +2031 -0
- package/package.json +52 -0
package/dclaude
ADDED
|
@@ -0,0 +1,2031 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# dclaude - Dockerized Claude Code Launcher
|
|
3
|
+
# https://github.com/alanbem/dclaude
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
# Configuration
|
|
8
|
+
readonly IMAGE_NAME="${DCLAUDE_REGISTRY:-docker.io}/alanbem/dclaude"
|
|
9
|
+
readonly IMAGE_TAG="${DCLAUDE_TAG:-latest}"
|
|
10
|
+
readonly IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
|
|
11
|
+
readonly VOLUME_PREFIX="dclaude"
|
|
12
|
+
readonly DEBUG="${DCLAUDE_DEBUG:-false}"
|
|
13
|
+
readonly QUIET="${DCLAUDE_QUIET:-false}"
|
|
14
|
+
readonly REMOVE_CONTAINER="${DCLAUDE_RM:-false}"
|
|
15
|
+
# Docker socket will be detected dynamically unless overridden
|
|
16
|
+
DOCKER_SOCKET="${DCLAUDE_DOCKER_SOCKET:-}"
|
|
17
|
+
readonly MOUNT_CONFIGS="${DCLAUDE_MOUNT_CONFIGS:-false}"
|
|
18
|
+
readonly GIT_AUTH_MODE="${DCLAUDE_GIT_AUTH:-auto}" # auto, agent-forwarding, key-mount, none
|
|
19
|
+
readonly ENABLE_SYSTEM_CONTEXT="${DCLAUDE_SYSTEM_CONTEXT:-true}" # Inform Claude about dclaude environment
|
|
20
|
+
|
|
21
|
+
# Chrome configuration (not readonly to allow --port flag override)
|
|
22
|
+
CHROME_PROFILE="${DCLAUDE_CHROME_PROFILE:-claude}"
|
|
23
|
+
CHROME_PORT="${DCLAUDE_CHROME_PORT:-9222}"
|
|
24
|
+
CHROME_BIN="${DCLAUDE_CHROME_BIN:-}"
|
|
25
|
+
CHROME_FLAGS="${DCLAUDE_CHROME_FLAGS:-}"
|
|
26
|
+
|
|
27
|
+
# Colors for output (only if terminal supports it)
|
|
28
|
+
if [[ -t 1 ]]; then
|
|
29
|
+
readonly RED='\033[0;31m'
|
|
30
|
+
readonly GREEN='\033[0;32m'
|
|
31
|
+
readonly YELLOW='\033[1;33m'
|
|
32
|
+
readonly BLUE='\033[0;34m'
|
|
33
|
+
readonly CYAN='\033[0;36m'
|
|
34
|
+
readonly NC='\033[0m' # No Color
|
|
35
|
+
else
|
|
36
|
+
readonly RED=''
|
|
37
|
+
readonly GREEN=''
|
|
38
|
+
readonly YELLOW=''
|
|
39
|
+
readonly BLUE=''
|
|
40
|
+
readonly CYAN=''
|
|
41
|
+
readonly NC=''
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Helper functions
|
|
45
|
+
error() {
|
|
46
|
+
echo -e "${RED}Error: $1${NC}" >&2
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
warning() {
|
|
50
|
+
echo -e "${YELLOW}Warning: $1${NC}" >&2
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
success() {
|
|
54
|
+
if [[ "$QUIET" != "true" ]]; then
|
|
55
|
+
echo -e "${GREEN}$1${NC}" >&2
|
|
56
|
+
fi
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
info() {
|
|
60
|
+
if [[ "$QUIET" != "true" ]]; then
|
|
61
|
+
echo -e "${BLUE}$1${NC}" >&2
|
|
62
|
+
fi
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
debug() {
|
|
66
|
+
# QUIET overrides DEBUG - if quiet, suppress debug too
|
|
67
|
+
if [[ "$QUIET" != "true" ]] && [[ "$DEBUG" == "true" ]]; then
|
|
68
|
+
echo -e "${CYAN}Debug: $1${NC}" >&2
|
|
69
|
+
fi
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Reset terminal mouse mode after tmux exits
|
|
73
|
+
# Prevents iTerm2 warnings about mouse reporting being left on
|
|
74
|
+
reset_terminal_mouse() {
|
|
75
|
+
# Disable all mouse reporting modes:
|
|
76
|
+
# ?1000 - X10 mouse reporting
|
|
77
|
+
# ?1002 - Cell motion mouse tracking
|
|
78
|
+
# ?1003 - All motion mouse tracking
|
|
79
|
+
# ?1006 - SGR extended mouse reporting
|
|
80
|
+
printf '\033[?1000l\033[?1002l\033[?1003l\033[?1006l'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Detect platform
|
|
84
|
+
detect_platform() {
|
|
85
|
+
case "$(uname -s)" in
|
|
86
|
+
Darwin*)
|
|
87
|
+
echo "darwin"
|
|
88
|
+
;;
|
|
89
|
+
Linux*)
|
|
90
|
+
echo "linux"
|
|
91
|
+
;;
|
|
92
|
+
MINGW*|CYGWIN*|MSYS*)
|
|
93
|
+
echo "windows"
|
|
94
|
+
;;
|
|
95
|
+
*)
|
|
96
|
+
echo "unknown"
|
|
97
|
+
;;
|
|
98
|
+
esac
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Detect Docker socket path based on platform
|
|
102
|
+
detect_docker_socket() {
|
|
103
|
+
info "Detecting Docker socket..."
|
|
104
|
+
|
|
105
|
+
# Prefer Docker context (tells us the active socket)
|
|
106
|
+
if command -v docker &> /dev/null && command -v jq &> /dev/null; then
|
|
107
|
+
debug "Checking Docker context for socket path"
|
|
108
|
+
local context_output context_socket
|
|
109
|
+
context_output=$(docker context inspect 2>/dev/null || true)
|
|
110
|
+
if [[ -n "$context_output" ]]; then
|
|
111
|
+
context_socket=$(echo "$context_output" | jq -r '.[0].Endpoints.docker.Host' 2>/dev/null | sed 's|unix://||' 2>/dev/null) || true
|
|
112
|
+
if [[ -n "$context_socket" ]] && [[ -S "$context_socket" ]]; then
|
|
113
|
+
debug "Docker socket found via context: $context_socket"
|
|
114
|
+
echo "$context_socket"
|
|
115
|
+
return 0
|
|
116
|
+
else
|
|
117
|
+
debug "Context socket not valid: ${context_socket:-<empty>}"
|
|
118
|
+
fi
|
|
119
|
+
else
|
|
120
|
+
debug "No Docker context found"
|
|
121
|
+
fi
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# Fallback to common socket paths
|
|
125
|
+
debug "Falling back to filesystem search for Docker socket"
|
|
126
|
+
local socket_paths=(
|
|
127
|
+
"$HOME/.docker/run/docker.sock" # macOS Docker Desktop
|
|
128
|
+
"/var/run/docker.sock" # Linux, Docker CE
|
|
129
|
+
"$HOME/.orbstack/run/docker.sock" # OrbStack
|
|
130
|
+
"$HOME/.colima/default/docker.sock" # Colima
|
|
131
|
+
"$HOME/.colima/docker.sock" # Colima alternative
|
|
132
|
+
"$HOME/.rd/docker.sock" # Rancher Desktop
|
|
133
|
+
"/run/user/$(id -u)/docker.sock" # Rootless Docker on Linux
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
for socket in "${socket_paths[@]}"; do
|
|
137
|
+
if [[ -S "$socket" ]]; then
|
|
138
|
+
debug "Docker socket found at: $socket"
|
|
139
|
+
echo "$socket"
|
|
140
|
+
return 0
|
|
141
|
+
fi
|
|
142
|
+
done
|
|
143
|
+
|
|
144
|
+
debug "No Docker socket found"
|
|
145
|
+
return 1
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Check if Docker is installed
|
|
149
|
+
check_docker() {
|
|
150
|
+
if ! command -v docker &> /dev/null; then
|
|
151
|
+
error "Docker is not installed. Please install Docker first."
|
|
152
|
+
echo "Visit: https://docs.docker.com/get-docker/" >&2
|
|
153
|
+
exit 1
|
|
154
|
+
fi
|
|
155
|
+
debug "Docker found at: $(command -v docker)"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Check if Docker daemon is running
|
|
159
|
+
check_docker_running() {
|
|
160
|
+
if ! docker info &> /dev/null; then
|
|
161
|
+
error "Docker daemon is not running. Please start Docker."
|
|
162
|
+
exit 1
|
|
163
|
+
fi
|
|
164
|
+
debug "Docker daemon is running"
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Find an available port in the specified range
|
|
168
|
+
find_available_port() {
|
|
169
|
+
local start_port=${1:-20000}
|
|
170
|
+
local end_port=${2:-65000}
|
|
171
|
+
local port
|
|
172
|
+
|
|
173
|
+
for ((port=start_port; port<=end_port; port++)); do
|
|
174
|
+
local port_in_use=false
|
|
175
|
+
|
|
176
|
+
# Fast check 1: netstat (catches most listening processes)
|
|
177
|
+
if command -v netstat &>/dev/null; then
|
|
178
|
+
if netstat -an 2>/dev/null | grep -qE "[.:]$port\s+(LISTEN|ESTABLISHED)" 2>/dev/null; then
|
|
179
|
+
port_in_use=true
|
|
180
|
+
fi
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# Fast check 2: lsof (more reliable on macOS, catches additional cases)
|
|
184
|
+
if ! $port_in_use && command -v lsof &>/dev/null; then
|
|
185
|
+
if lsof -i ":$port" &>/dev/null 2>&1; then
|
|
186
|
+
port_in_use=true
|
|
187
|
+
fi
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
# Comprehensive check: Socket test (catches Docker containers, VMs, kernel-level bindings)
|
|
191
|
+
# Only run if fast checks didn't detect anything (avoids timeout overhead)
|
|
192
|
+
if ! $port_in_use; then
|
|
193
|
+
if timeout 0.1 bash -c "exec 3<>/dev/tcp/localhost/$port" 2>/dev/null; then
|
|
194
|
+
# Successfully connected - something is listening
|
|
195
|
+
exec 3<&- 2>/dev/null # Close connection
|
|
196
|
+
port_in_use=true
|
|
197
|
+
fi
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
if ! $port_in_use; then
|
|
201
|
+
echo "$port"
|
|
202
|
+
return 0
|
|
203
|
+
fi
|
|
204
|
+
done
|
|
205
|
+
|
|
206
|
+
# Fallback: use a random port if no available port found
|
|
207
|
+
echo $((20000 + RANDOM % 45000))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Portable timeout function that works on both Linux and macOS
|
|
211
|
+
# Usage: run_with_timeout <seconds> <command...>
|
|
212
|
+
run_with_timeout() {
|
|
213
|
+
local timeout_seconds=$1
|
|
214
|
+
shift
|
|
215
|
+
|
|
216
|
+
# Try to use timeout command if available (Linux, brew-installed coreutils)
|
|
217
|
+
if command -v timeout &>/dev/null; then
|
|
218
|
+
timeout "$timeout_seconds" "$@"
|
|
219
|
+
return $?
|
|
220
|
+
elif command -v gtimeout &>/dev/null; then
|
|
221
|
+
# macOS with coreutils installed via Homebrew
|
|
222
|
+
gtimeout "$timeout_seconds" "$@"
|
|
223
|
+
return $?
|
|
224
|
+
else
|
|
225
|
+
# Fallback: run command in background and kill after timeout
|
|
226
|
+
local pid
|
|
227
|
+
"$@" &
|
|
228
|
+
pid=$!
|
|
229
|
+
|
|
230
|
+
# Sleep in background and kill the process after timeout
|
|
231
|
+
(
|
|
232
|
+
sleep "$timeout_seconds"
|
|
233
|
+
kill -TERM "$pid" 2>/dev/null || true
|
|
234
|
+
) &
|
|
235
|
+
local timer_pid=$!
|
|
236
|
+
|
|
237
|
+
# Wait for the command to finish
|
|
238
|
+
local result
|
|
239
|
+
if wait "$pid" 2>/dev/null; then
|
|
240
|
+
result=$?
|
|
241
|
+
kill -TERM "$timer_pid" 2>/dev/null || true
|
|
242
|
+
wait "$timer_pid" 2>/dev/null || true
|
|
243
|
+
return $result
|
|
244
|
+
else
|
|
245
|
+
# Command failed or was killed
|
|
246
|
+
return 124 # Standard timeout exit code
|
|
247
|
+
fi
|
|
248
|
+
fi
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
# Detect network capability (host vs bridge)
|
|
252
|
+
detect_network_capability() {
|
|
253
|
+
info "Detecting network mode..."
|
|
254
|
+
|
|
255
|
+
local cache_dir="${HOME}/.dclaude"
|
|
256
|
+
local cache_file="${cache_dir}/network-mode"
|
|
257
|
+
local test_timeout=10
|
|
258
|
+
local server_container=""
|
|
259
|
+
|
|
260
|
+
# Cleanup function for this function's containers
|
|
261
|
+
cleanup_nettest() {
|
|
262
|
+
if [[ -n "$server_container" ]]; then
|
|
263
|
+
debug "Cleaning up test container: $server_container"
|
|
264
|
+
docker stop "$server_container" &>/dev/null || true
|
|
265
|
+
docker rm -f "$server_container" &>/dev/null || true
|
|
266
|
+
fi
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# Set up trap for cleanup
|
|
270
|
+
trap cleanup_nettest EXIT
|
|
271
|
+
|
|
272
|
+
# Create cache directory if it doesn't exist
|
|
273
|
+
if [[ ! -d "$cache_dir" ]]; then
|
|
274
|
+
mkdir -p "$cache_dir" || {
|
|
275
|
+
debug "Failed to create cache directory, proceeding without cache"
|
|
276
|
+
cache_file=""
|
|
277
|
+
}
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
# Check cache first (valid for 24 hours)
|
|
281
|
+
if [[ -n "$cache_file" && -f "$cache_file" ]]; then
|
|
282
|
+
local cache_age
|
|
283
|
+
if cache_age=$(stat -c %Y "$cache_file" 2>/dev/null) || cache_age=$(stat -f %m "$cache_file" 2>/dev/null); then
|
|
284
|
+
local current_time
|
|
285
|
+
current_time=$(date +%s)
|
|
286
|
+
local age_hours=$(( (current_time - cache_age) / 3600 ))
|
|
287
|
+
|
|
288
|
+
if [[ $age_hours -lt 24 ]]; then
|
|
289
|
+
local cached_mode
|
|
290
|
+
if cached_mode=$(cat "$cache_file" 2>/dev/null) && [[ "$cached_mode" =~ ^(host|bridge)$ ]]; then
|
|
291
|
+
debug "Using cached network mode: $cached_mode (age: ${age_hours}h)"
|
|
292
|
+
trap - EXIT
|
|
293
|
+
echo "$cached_mode"
|
|
294
|
+
return 0
|
|
295
|
+
fi
|
|
296
|
+
fi
|
|
297
|
+
fi
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
debug "Testing network capabilities..."
|
|
301
|
+
|
|
302
|
+
# Ensure alpine:3.19 is available
|
|
303
|
+
if ! docker image inspect alpine:3.19 &>/dev/null; then
|
|
304
|
+
debug "Alpine 3.19 image not found, pulling..."
|
|
305
|
+
if ! docker pull alpine:3.19 &>/dev/null; then
|
|
306
|
+
debug "Failed to pull alpine:3.19, falling back to bridge mode"
|
|
307
|
+
trap - EXIT
|
|
308
|
+
echo "bridge" | tee "$cache_file" 2>/dev/null || echo "bridge"
|
|
309
|
+
return 0
|
|
310
|
+
fi
|
|
311
|
+
debug "Alpine 3.19 image pulled successfully"
|
|
312
|
+
else
|
|
313
|
+
debug "Alpine 3.19 image is available"
|
|
314
|
+
fi
|
|
315
|
+
|
|
316
|
+
# Test 1: Basic host networking support
|
|
317
|
+
debug "Test 1: Basic host networking support"
|
|
318
|
+
if ! run_with_timeout "$test_timeout" docker run --rm --network host alpine:3.19 \
|
|
319
|
+
sh -c 'ip addr show lo | grep -q "127\.0\.0\.1"' &>/dev/null; then
|
|
320
|
+
debug "Host networking not supported (basic test failed)"
|
|
321
|
+
trap - EXIT
|
|
322
|
+
echo "bridge" | tee "$cache_file" 2>/dev/null || echo "bridge"
|
|
323
|
+
return 0
|
|
324
|
+
fi
|
|
325
|
+
|
|
326
|
+
# Test 2: Container-to-container localhost access
|
|
327
|
+
debug "Test 2: Container-to-container localhost access"
|
|
328
|
+
local test_port
|
|
329
|
+
test_port=$(find_available_port)
|
|
330
|
+
debug "Using dynamic port: $test_port"
|
|
331
|
+
server_container="dclaude-nettest-$$"
|
|
332
|
+
|
|
333
|
+
# Start test server
|
|
334
|
+
if ! docker run --rm -d --name "$server_container" --network host alpine:3.19 \
|
|
335
|
+
sh -c "echo 'test-response' | nc -l -p $test_port" &>/dev/null; then
|
|
336
|
+
debug "Failed to start test server"
|
|
337
|
+
trap - EXIT
|
|
338
|
+
echo "bridge" | tee "$cache_file" 2>/dev/null || echo "bridge"
|
|
339
|
+
return 0
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
# Wait for server to be ready (max 5 seconds)
|
|
343
|
+
debug "Waiting for test server to be ready..."
|
|
344
|
+
local ready=false
|
|
345
|
+
local wait_count=0
|
|
346
|
+
local max_wait=50 # 5 seconds (50 * 0.1 seconds)
|
|
347
|
+
|
|
348
|
+
while [[ $wait_count -lt $max_wait ]]; do
|
|
349
|
+
# Use more portable netstat options for Alpine Linux
|
|
350
|
+
if docker exec "$server_container" sh -c "netstat -tuln 2>/dev/null | grep -q \":$test_port \"" &>/dev/null; then
|
|
351
|
+
ready=true
|
|
352
|
+
debug "Test server is ready after $((wait_count * 100))ms"
|
|
353
|
+
break
|
|
354
|
+
fi
|
|
355
|
+
sleep 0.1
|
|
356
|
+
((wait_count++))
|
|
357
|
+
done
|
|
358
|
+
|
|
359
|
+
if [[ "$ready" != "true" ]]; then
|
|
360
|
+
debug "Test server failed to become ready within 5 seconds"
|
|
361
|
+
cleanup_nettest
|
|
362
|
+
trap - EXIT
|
|
363
|
+
echo "bridge" | tee "$cache_file" 2>/dev/null || echo "bridge"
|
|
364
|
+
return 0
|
|
365
|
+
fi
|
|
366
|
+
|
|
367
|
+
# Test client connection
|
|
368
|
+
local test_result="bridge"
|
|
369
|
+
if run_with_timeout 5 docker run --rm --network host alpine:3.19 \
|
|
370
|
+
sh -c "echo '' | nc localhost $test_port" &>/dev/null; then
|
|
371
|
+
test_result="host"
|
|
372
|
+
debug "Host networking fully supported"
|
|
373
|
+
else
|
|
374
|
+
debug "Host networking partially supported but localhost access failed"
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
# Cleanup test server (handled by trap)
|
|
378
|
+
cleanup_nettest
|
|
379
|
+
|
|
380
|
+
# Cache the result
|
|
381
|
+
if [[ -n "$cache_file" ]]; then
|
|
382
|
+
echo "$test_result" > "$cache_file" 2>/dev/null || true
|
|
383
|
+
fi
|
|
384
|
+
|
|
385
|
+
# Platform-specific validation of detected mode
|
|
386
|
+
local platform
|
|
387
|
+
platform=$(detect_platform)
|
|
388
|
+
case "$platform" in
|
|
389
|
+
darwin)
|
|
390
|
+
if [[ "$test_result" == "host" ]]; then
|
|
391
|
+
debug "Host networking detected on macOS - validating compatibility"
|
|
392
|
+
# On macOS, host networking should only work with Docker Desktop beta or OrbStack
|
|
393
|
+
if ! docker system info 2>/dev/null | grep -qi "orbstack\|desktop.*beta" &>/dev/null; then
|
|
394
|
+
debug "Host networking may not work properly on this Docker setup for macOS"
|
|
395
|
+
warning "Host networking detected but may not work properly on macOS Docker Desktop (non-beta)"
|
|
396
|
+
fi
|
|
397
|
+
fi
|
|
398
|
+
;;
|
|
399
|
+
windows)
|
|
400
|
+
if [[ "$test_result" == "host" ]]; then
|
|
401
|
+
debug "Host networking detected on Windows - this is unusual"
|
|
402
|
+
warning "Host networking detected on Windows - this may indicate WSL2 or special configuration"
|
|
403
|
+
fi
|
|
404
|
+
;;
|
|
405
|
+
linux)
|
|
406
|
+
debug "Network mode '$test_result' is expected on Linux platform"
|
|
407
|
+
;;
|
|
408
|
+
*)
|
|
409
|
+
debug "Unknown platform '$platform' - network mode validation skipped"
|
|
410
|
+
;;
|
|
411
|
+
esac
|
|
412
|
+
|
|
413
|
+
# Clear the trap before returning
|
|
414
|
+
trap - EXIT
|
|
415
|
+
|
|
416
|
+
debug "Network capability detection result: $test_result"
|
|
417
|
+
echo "$test_result"
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
# Create Docker volumes if they don't exist
|
|
421
|
+
create_volumes() {
|
|
422
|
+
# Create essential volumes for persistence
|
|
423
|
+
local volumes=("${VOLUME_PREFIX}-claude" "${VOLUME_PREFIX}-config")
|
|
424
|
+
|
|
425
|
+
for volume in "${volumes[@]}"; do
|
|
426
|
+
if ! docker volume inspect "$volume" &> /dev/null; then
|
|
427
|
+
info "Creating volume: $volume"
|
|
428
|
+
if ! docker volume create "$volume" > /dev/null; then
|
|
429
|
+
error "Failed to create volume: $volume"
|
|
430
|
+
exit 1
|
|
431
|
+
fi
|
|
432
|
+
debug "Volume created successfully: $volume"
|
|
433
|
+
else
|
|
434
|
+
debug "Volume exists: $volume"
|
|
435
|
+
fi
|
|
436
|
+
done
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
# Pull or update the Docker image
|
|
440
|
+
update_image() {
|
|
441
|
+
# Skip updates if DCLAUDE_NO_UPDATE is set
|
|
442
|
+
if [[ "${DCLAUDE_NO_UPDATE:-false}" == "true" ]]; then
|
|
443
|
+
debug "Skipping image update (DCLAUDE_NO_UPDATE=true)"
|
|
444
|
+
# Ensure image exists
|
|
445
|
+
if ! docker image inspect "$IMAGE" &> /dev/null; then
|
|
446
|
+
error "Docker image $IMAGE not found locally and updates disabled."
|
|
447
|
+
error "Either unset DCLAUDE_NO_UPDATE or pull the image manually."
|
|
448
|
+
exit 1
|
|
449
|
+
fi
|
|
450
|
+
return
|
|
451
|
+
fi
|
|
452
|
+
|
|
453
|
+
local current_id=""
|
|
454
|
+
local new_id=""
|
|
455
|
+
|
|
456
|
+
# Get current image ID if exists
|
|
457
|
+
if docker image inspect "$IMAGE" &> /dev/null; then
|
|
458
|
+
current_id=$(docker image inspect "$IMAGE" --format='{{.Id}}')
|
|
459
|
+
debug "Current image ID: ${current_id:0:12}"
|
|
460
|
+
fi
|
|
461
|
+
|
|
462
|
+
# Try to pull latest
|
|
463
|
+
info "Checking for updates to $IMAGE..."
|
|
464
|
+
if docker pull "$IMAGE" 2> /dev/null; then
|
|
465
|
+
new_id=$(docker image inspect "$IMAGE" --format='{{.Id}}' 2>/dev/null || echo "")
|
|
466
|
+
|
|
467
|
+
if [[ -z "$new_id" ]]; then
|
|
468
|
+
warning "Failed to inspect image after pull"
|
|
469
|
+
elif [[ "$current_id" != "$new_id" ]]; then
|
|
470
|
+
success "Image updated successfully."
|
|
471
|
+
debug "New image ID: ${new_id:0:12}"
|
|
472
|
+
else
|
|
473
|
+
debug "Image is up to date"
|
|
474
|
+
fi
|
|
475
|
+
else
|
|
476
|
+
if [[ -z "$current_id" ]]; then
|
|
477
|
+
error "Failed to pull image and no local image exists."
|
|
478
|
+
exit 1
|
|
479
|
+
else
|
|
480
|
+
warning "Could not check for updates, using local image."
|
|
481
|
+
fi
|
|
482
|
+
fi
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# Get the host path for mounting
|
|
486
|
+
get_host_path() {
|
|
487
|
+
local host_path
|
|
488
|
+
host_path=$(pwd) || {
|
|
489
|
+
error "Failed to get current directory"
|
|
490
|
+
exit 1
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
# Basic path validation
|
|
494
|
+
if [[ ! -d "$host_path" ]]; then
|
|
495
|
+
error "Current directory does not exist: $host_path"
|
|
496
|
+
exit 1
|
|
497
|
+
fi
|
|
498
|
+
|
|
499
|
+
echo "$host_path"
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
# Generate deterministic container name from path
|
|
503
|
+
get_container_name() {
|
|
504
|
+
local path="${1:-$HOST_PATH}"
|
|
505
|
+
local path_hash
|
|
506
|
+
path_hash=$(echo -n "$path" | md5sum 2>/dev/null | cut -d' ' -f1 || echo -n "$path" | md5 | cut -d' ' -f1)
|
|
507
|
+
echo "dclaude-${path_hash:0:12}"
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
# Generate system context prompt for Claude
|
|
511
|
+
generate_system_context() {
|
|
512
|
+
local network_mode="${1:-auto}"
|
|
513
|
+
local git_auth_mode="${2:-auto}"
|
|
514
|
+
local has_docker="${3:-false}"
|
|
515
|
+
local platform="${4:-unknown}"
|
|
516
|
+
local docker_socket="${5:-}"
|
|
517
|
+
|
|
518
|
+
cat <<'EOF'
|
|
519
|
+
|
|
520
|
+
# dclaude Environment Context
|
|
521
|
+
|
|
522
|
+
You are running inside **dclaude** - a Docker container that emulates the host environment. This is important context for understanding your capabilities and limitations:
|
|
523
|
+
|
|
524
|
+
## Container Architecture
|
|
525
|
+
- **Host Emulation**: Your current working directory is mounted at the exact same path as on the host
|
|
526
|
+
- **Path Mirroring**: All file paths work identically to native execution (e.g., `/Users/alice/project` in container = same path on host)
|
|
527
|
+
- **Isolated Environment**: While you operate in a container, file operations affect the real host filesystem through volume mounts
|
|
528
|
+
|
|
529
|
+
## Host Environment
|
|
530
|
+
EOF
|
|
531
|
+
|
|
532
|
+
# Platform info
|
|
533
|
+
case "$platform" in
|
|
534
|
+
darwin)
|
|
535
|
+
echo "- **Host OS**: macOS"
|
|
536
|
+
;;
|
|
537
|
+
linux)
|
|
538
|
+
echo "- **Host OS**: Linux"
|
|
539
|
+
;;
|
|
540
|
+
windows)
|
|
541
|
+
echo "- **Host OS**: Windows"
|
|
542
|
+
;;
|
|
543
|
+
*)
|
|
544
|
+
echo "- **Host OS**: Unknown"
|
|
545
|
+
;;
|
|
546
|
+
esac
|
|
547
|
+
|
|
548
|
+
# Architecture
|
|
549
|
+
local arch=$(uname -m 2>/dev/null || echo "unknown")
|
|
550
|
+
case "$arch" in
|
|
551
|
+
arm64|aarch64)
|
|
552
|
+
echo "- **Architecture**: ARM64 (Apple Silicon / ARM)"
|
|
553
|
+
;;
|
|
554
|
+
x86_64|amd64)
|
|
555
|
+
echo "- **Architecture**: x86_64 (Intel/AMD)"
|
|
556
|
+
;;
|
|
557
|
+
*)
|
|
558
|
+
echo "- **Architecture**: $arch"
|
|
559
|
+
;;
|
|
560
|
+
esac
|
|
561
|
+
|
|
562
|
+
# Docker provider detection from socket path
|
|
563
|
+
if [[ -n "$docker_socket" ]]; then
|
|
564
|
+
case "$docker_socket" in
|
|
565
|
+
*/.orbstack/*)
|
|
566
|
+
echo "- **Docker Provider**: OrbStack (high performance, native macOS integration)"
|
|
567
|
+
;;
|
|
568
|
+
*/.docker/run/*)
|
|
569
|
+
echo "- **Docker Provider**: Docker Desktop"
|
|
570
|
+
;;
|
|
571
|
+
*/.colima/*)
|
|
572
|
+
echo "- **Docker Provider**: Colima"
|
|
573
|
+
;;
|
|
574
|
+
*/.rd/*)
|
|
575
|
+
echo "- **Docker Provider**: Rancher Desktop"
|
|
576
|
+
;;
|
|
577
|
+
/var/run/docker.sock)
|
|
578
|
+
echo "- **Docker Provider**: Docker Engine (native Linux)"
|
|
579
|
+
;;
|
|
580
|
+
*)
|
|
581
|
+
echo "- **Docker Provider**: Custom Docker setup"
|
|
582
|
+
;;
|
|
583
|
+
esac
|
|
584
|
+
fi
|
|
585
|
+
|
|
586
|
+
cat <<'EOF'
|
|
587
|
+
|
|
588
|
+
## Available Capabilities
|
|
589
|
+
EOF
|
|
590
|
+
|
|
591
|
+
# Docker access
|
|
592
|
+
if [[ "$has_docker" == "true" ]]; then
|
|
593
|
+
cat <<'EOF'
|
|
594
|
+
- **Docker Access**: You have access to the host's Docker daemon via mounted socket
|
|
595
|
+
- Can build, run, and manage Docker containers
|
|
596
|
+
- Can execute docker and docker-compose commands
|
|
597
|
+
- All Docker operations affect the host Docker daemon
|
|
598
|
+
EOF
|
|
599
|
+
fi
|
|
600
|
+
|
|
601
|
+
# Network mode
|
|
602
|
+
case "$network_mode" in
|
|
603
|
+
host)
|
|
604
|
+
cat <<'EOF'
|
|
605
|
+
- **Networking**: Host networking mode enabled
|
|
606
|
+
- Direct access to `localhost:PORT` services on the host
|
|
607
|
+
- Can communicate with other containers via localhost
|
|
608
|
+
- Full network stack sharing with host
|
|
609
|
+
EOF
|
|
610
|
+
;;
|
|
611
|
+
bridge)
|
|
612
|
+
cat <<'EOF'
|
|
613
|
+
- **Networking**: Bridge networking mode (isolated network)
|
|
614
|
+
- Cannot directly access `localhost` services
|
|
615
|
+
- Use `host.docker.internal:PORT` to access host services
|
|
616
|
+
- Container has isolated network namespace
|
|
617
|
+
EOF
|
|
618
|
+
;;
|
|
619
|
+
esac
|
|
620
|
+
|
|
621
|
+
# Git SSH authentication
|
|
622
|
+
case "$git_auth_mode" in
|
|
623
|
+
agent-forwarding)
|
|
624
|
+
cat <<'EOF'
|
|
625
|
+
- **SSH Authentication**: Agent forwarding enabled
|
|
626
|
+
- SSH keys available via forwarded agent (secure, keys never in container)
|
|
627
|
+
- Can authenticate to GitHub, GitLab, and other SSH services
|
|
628
|
+
- Private keys remain on host machine only
|
|
629
|
+
- **Important**: Keys must be loaded in host's SSH agent (`ssh-add -l` to verify)
|
|
630
|
+
- On macOS: Proxy container (`dclaude-ssh-proxy-*`) bridges permissions automatically
|
|
631
|
+
- If git fails with auth errors, user needs to run `ssh-add ~/.ssh/id_ed25519` on host
|
|
632
|
+
- Socket location in container: $SSH_AUTH_SOCK (typically /tmp/ssh-proxy/agent)
|
|
633
|
+
- Known hosts pre-configured for: GitHub, GitLab, Bitbucket
|
|
634
|
+
EOF
|
|
635
|
+
;;
|
|
636
|
+
key-mount)
|
|
637
|
+
cat <<'EOF'
|
|
638
|
+
- **SSH Authentication**: SSH keys mounted (read-only)
|
|
639
|
+
- Host's ~/.ssh directory mounted into container at /home/claude/.ssh
|
|
640
|
+
- Can authenticate to GitHub, GitLab, and other SSH services
|
|
641
|
+
- Keys are read-only, cannot be modified
|
|
642
|
+
- SSH agent not required - keys read directly from filesystem
|
|
643
|
+
- Use ssh-add inside container if agent needed
|
|
644
|
+
- Note: Uses host's known_hosts file (not container's pre-configured one)
|
|
645
|
+
EOF
|
|
646
|
+
;;
|
|
647
|
+
none)
|
|
648
|
+
cat <<'EOF'
|
|
649
|
+
- **SSH Authentication**: Not configured
|
|
650
|
+
- SSH keys not available in container
|
|
651
|
+
- Cannot authenticate to remote Git repositories via SSH
|
|
652
|
+
- Recommend using HTTPS authentication for Git operations
|
|
653
|
+
- Or restart with DCLAUDE_GIT_AUTH=agent-forwarding or DCLAUDE_GIT_AUTH=key-mount
|
|
654
|
+
EOF
|
|
655
|
+
;;
|
|
656
|
+
esac
|
|
657
|
+
|
|
658
|
+
cat <<'EOF'
|
|
659
|
+
|
|
660
|
+
## Development Tools Available
|
|
661
|
+
- **Languages**: Node.js 20+, Python 3
|
|
662
|
+
- **Package Managers**: npm, pip, Homebrew/Linuxbrew
|
|
663
|
+
- **Tools**: git, gh (GitHub CLI), docker, docker-compose, curl, tmux, nano
|
|
664
|
+
- **Shell**: bash (your commands execute in bash shell)
|
|
665
|
+
|
|
666
|
+
## Git Configuration Requirements
|
|
667
|
+
**IMPORTANT**: Before performing any git operations (commit, push, etc.), you MUST:
|
|
668
|
+
1. Check if git is configured: `git config user.name` and `git config user.email`
|
|
669
|
+
2. If either is missing/empty, ASK the user for their git name and email
|
|
670
|
+
3. Configure git with: `git config --global user.name "User Name"` and `git config --global user.email "user@example.com"`
|
|
671
|
+
4. Never assume or make up git credentials - always ask the user first
|
|
672
|
+
|
|
673
|
+
## Important Notes
|
|
674
|
+
- File operations are performed on the host filesystem (not isolated)
|
|
675
|
+
- Changes persist after container exits (files are on host)
|
|
676
|
+
- System packages can be installed with apt-get, Homebrew, or npm
|
|
677
|
+
- Use relative paths when possible - absolute paths work due to path mirroring
|
|
678
|
+
- Git operations work normally - repository sees correct paths
|
|
679
|
+
|
|
680
|
+
When suggesting commands or file operations, you can treat this environment as if running natively on the host, with the benefits of containerization for tool isolation.
|
|
681
|
+
EOF
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
# Handle SSH authentication based on DCLAUDE_GIT_AUTH mode
|
|
685
|
+
# Setup SSH proxy container for macOS
|
|
686
|
+
setup_ssh_proxy_container() {
|
|
687
|
+
local proxy_container="dclaude-ssh-proxy-$(id -u)"
|
|
688
|
+
|
|
689
|
+
# Check if proxy container already exists and is running
|
|
690
|
+
if docker ps -q -f name="^${proxy_container}$" 2>/dev/null | grep -q .; then
|
|
691
|
+
debug "SSH proxy container already running"
|
|
692
|
+
return 0
|
|
693
|
+
fi
|
|
694
|
+
|
|
695
|
+
# Remove any stopped proxy container
|
|
696
|
+
if docker ps -aq -f name="^${proxy_container}$" 2>/dev/null | grep -q .; then
|
|
697
|
+
debug "Removing stopped SSH proxy container"
|
|
698
|
+
docker rm -f "$proxy_container" >/dev/null 2>&1
|
|
699
|
+
fi
|
|
700
|
+
|
|
701
|
+
info "Starting SSH agent proxy container..."
|
|
702
|
+
debug "Proxy container name: $proxy_container"
|
|
703
|
+
debug "Bridging macOS SSH agent socket permissions"
|
|
704
|
+
|
|
705
|
+
# Create the proxy container that runs socat as root
|
|
706
|
+
# This container just bridges the permission gap and exits
|
|
707
|
+
docker run -d \
|
|
708
|
+
--name "$proxy_container" \
|
|
709
|
+
-v "/run/host-services/ssh-auth.sock:/run/host-services/ssh-auth.sock:ro" \
|
|
710
|
+
-v "dclaude-ssh-proxy:/tmp/ssh-proxy" \
|
|
711
|
+
--rm \
|
|
712
|
+
alpine:3.19 sh -c '
|
|
713
|
+
# Install socat
|
|
714
|
+
apk add --no-cache socat >/dev/null 2>&1
|
|
715
|
+
|
|
716
|
+
# Create proxy socket accessible to all users
|
|
717
|
+
rm -f /tmp/ssh-proxy/agent
|
|
718
|
+
socat UNIX-LISTEN:/tmp/ssh-proxy/agent,fork,mode=660 \
|
|
719
|
+
UNIX-CONNECT:/run/host-services/ssh-auth.sock
|
|
720
|
+
' >/dev/null 2>&1
|
|
721
|
+
|
|
722
|
+
# Give it a moment to start
|
|
723
|
+
sleep 0.5
|
|
724
|
+
|
|
725
|
+
# Verify the proxy is working
|
|
726
|
+
if ! docker ps -q -f name="^${proxy_container}$" 2>/dev/null | grep -q .; then
|
|
727
|
+
error "Failed to start SSH proxy container"
|
|
728
|
+
return 1
|
|
729
|
+
fi
|
|
730
|
+
|
|
731
|
+
debug "SSH proxy container started successfully"
|
|
732
|
+
return 0
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
handle_git_auth() {
|
|
736
|
+
local docker_args=()
|
|
737
|
+
local git_auth_mode="${GIT_AUTH_MODE}"
|
|
738
|
+
|
|
739
|
+
# If config mounting is enabled and git auth mode is auto, prefer key-mount for consistency
|
|
740
|
+
if [[ "$MOUNT_CONFIGS" == "true" ]] && [[ "$git_auth_mode" == "auto" ]]; then
|
|
741
|
+
if [[ -d "${HOME}/.ssh" ]] && [[ -r "${HOME}/.ssh" ]]; then
|
|
742
|
+
git_auth_mode="key-mount"
|
|
743
|
+
debug "Git auth: key-mount (DCLAUDE_MOUNT_CONFIGS=true prefers key mounting)"
|
|
744
|
+
fi
|
|
745
|
+
fi
|
|
746
|
+
|
|
747
|
+
# Auto-detect best method if set to auto
|
|
748
|
+
if [[ "$git_auth_mode" == "auto" ]]; then
|
|
749
|
+
if [[ -n "${SSH_AUTH_SOCK:-}" ]] && [[ -S "${SSH_AUTH_SOCK}" ]]; then
|
|
750
|
+
git_auth_mode="agent-forwarding"
|
|
751
|
+
debug "Git auth: agent-forwarding (auto-detected active agent)"
|
|
752
|
+
elif [[ -d "${HOME}/.ssh" ]] && [[ -r "${HOME}/.ssh" ]]; then
|
|
753
|
+
git_auth_mode="key-mount"
|
|
754
|
+
debug "Git auth: key-mount (auto-detected SSH directory)"
|
|
755
|
+
else
|
|
756
|
+
git_auth_mode="none"
|
|
757
|
+
debug "Git auth: none (no SSH agent or keys found)"
|
|
758
|
+
fi
|
|
759
|
+
fi
|
|
760
|
+
|
|
761
|
+
case "$git_auth_mode" in
|
|
762
|
+
agent-forwarding)
|
|
763
|
+
if [[ -z "${SSH_AUTH_SOCK:-}" ]]; then
|
|
764
|
+
warning "SSH agent forwarding requested but SSH_AUTH_SOCK not set"
|
|
765
|
+
warning "Start SSH agent with: eval \$(ssh-agent) && ssh-add"
|
|
766
|
+
return 1
|
|
767
|
+
fi
|
|
768
|
+
|
|
769
|
+
if [[ ! -S "${SSH_AUTH_SOCK}" ]]; then
|
|
770
|
+
warning "SSH agent forwarding requested but socket not found: ${SSH_AUTH_SOCK}"
|
|
771
|
+
return 1
|
|
772
|
+
fi
|
|
773
|
+
|
|
774
|
+
# Platform-specific socket mounting
|
|
775
|
+
local platform=$(detect_platform)
|
|
776
|
+
debug "Setting up SSH agent forwarding for platform: $platform"
|
|
777
|
+
case "$platform" in
|
|
778
|
+
linux)
|
|
779
|
+
debug "Using direct socket mount: ${SSH_AUTH_SOCK}"
|
|
780
|
+
docker_args+=(-v "${SSH_AUTH_SOCK}:/tmp/ssh-agent" -e "SSH_AUTH_SOCK=/tmp/ssh-agent")
|
|
781
|
+
info "SSH agent forwarding enabled (Linux)"
|
|
782
|
+
;;
|
|
783
|
+
darwin)
|
|
784
|
+
# On macOS, we need to set up a proxy container first
|
|
785
|
+
debug "Setting up SSH proxy container for macOS"
|
|
786
|
+
setup_ssh_proxy_container
|
|
787
|
+
|
|
788
|
+
# Now mount the proxied socket from the shared volume
|
|
789
|
+
docker_args+=(-v "dclaude-ssh-proxy:/tmp/ssh-proxy:ro"
|
|
790
|
+
-e "SSH_AUTH_SOCK=/tmp/ssh-proxy/agent")
|
|
791
|
+
info "SSH agent forwarding enabled via proxy container"
|
|
792
|
+
debug "Mounted SSH proxy volume: dclaude-ssh-proxy:/tmp/ssh-proxy"
|
|
793
|
+
;;
|
|
794
|
+
windows)
|
|
795
|
+
warning "SSH agent forwarding not fully supported on Windows"
|
|
796
|
+
warning "Consider using key-mount mode instead: DCLAUDE_GIT_AUTH=key-mount"
|
|
797
|
+
return 1
|
|
798
|
+
;;
|
|
799
|
+
esac
|
|
800
|
+
;;
|
|
801
|
+
|
|
802
|
+
key-mount)
|
|
803
|
+
if [[ -d "${HOME}/.ssh" ]] && [[ -r "${HOME}/.ssh" ]]; then
|
|
804
|
+
docker_args+=(-v "${HOME}/.ssh:/home/claude/.ssh:ro")
|
|
805
|
+
info "SSH key mounting enabled (read-only)"
|
|
806
|
+
debug "Mounting SSH directory: ${HOME}/.ssh"
|
|
807
|
+
warning "SSH private keys are accessible in container (read-only)"
|
|
808
|
+
else
|
|
809
|
+
warning "SSH key mount requested but ~/.ssh not found or not readable"
|
|
810
|
+
return 1
|
|
811
|
+
fi
|
|
812
|
+
;;
|
|
813
|
+
|
|
814
|
+
none)
|
|
815
|
+
debug "SSH authentication disabled"
|
|
816
|
+
;;
|
|
817
|
+
|
|
818
|
+
*)
|
|
819
|
+
error "Invalid git auth mode: $git_auth_mode (valid: auto, agent-forwarding, key-mount, none)"
|
|
820
|
+
return 1
|
|
821
|
+
;;
|
|
822
|
+
esac
|
|
823
|
+
|
|
824
|
+
# Print arguments separated by null characters for safe parsing (only if we have args)
|
|
825
|
+
if [[ ${#docker_args[@]} -gt 0 ]]; then
|
|
826
|
+
printf '%s\0' "${docker_args[@]}"
|
|
827
|
+
fi
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
# Mount configuration directories from host
|
|
831
|
+
mount_host_configs() {
|
|
832
|
+
local docker_args=()
|
|
833
|
+
local mounted_count=0
|
|
834
|
+
|
|
835
|
+
if [[ "$MOUNT_CONFIGS" != "true" ]]; then
|
|
836
|
+
debug "Config mounting disabled (DCLAUDE_MOUNT_CONFIGS=$MOUNT_CONFIGS)"
|
|
837
|
+
return 0
|
|
838
|
+
fi
|
|
839
|
+
|
|
840
|
+
info "Mounting host configurations (read-only)"
|
|
841
|
+
debug "SSH authentication handled separately via DCLAUDE_GIT_AUTH"
|
|
842
|
+
|
|
843
|
+
# Docker configuration (docker-cli installed)
|
|
844
|
+
# Default to true when master switch is enabled
|
|
845
|
+
if [[ "${DCLAUDE_MOUNT_DOCKER:-true}" == "true" ]] && [[ -d "${HOME}/.docker" ]]; then
|
|
846
|
+
if [[ -r "${HOME}/.docker" ]]; then
|
|
847
|
+
docker_args+=(-v "${HOME}/.docker:/home/claude/.docker:ro")
|
|
848
|
+
debug "Mounting Docker config: ${HOME}/.docker"
|
|
849
|
+
((mounted_count++))
|
|
850
|
+
else
|
|
851
|
+
warning "Docker config exists but is not readable: ${HOME}/.docker"
|
|
852
|
+
fi
|
|
853
|
+
elif [[ "${DCLAUDE_MOUNT_DOCKER:-true}" == "true" ]]; then
|
|
854
|
+
debug "Docker config not found: ${HOME}/.docker"
|
|
855
|
+
fi
|
|
856
|
+
|
|
857
|
+
# GitHub CLI configuration (github-cli installed)
|
|
858
|
+
# Default to true when master switch is enabled
|
|
859
|
+
if [[ "${DCLAUDE_MOUNT_GH:-true}" == "true" ]] && [[ -d "${HOME}/.config/gh" ]]; then
|
|
860
|
+
if [[ -r "${HOME}/.config/gh" ]]; then
|
|
861
|
+
docker_args+=(-v "${HOME}/.config/gh:/home/claude/.config/gh:ro")
|
|
862
|
+
debug "Mounting GitHub CLI config: ${HOME}/.config/gh"
|
|
863
|
+
((mounted_count++))
|
|
864
|
+
else
|
|
865
|
+
warning "GitHub CLI config exists but is not readable: ${HOME}/.config/gh"
|
|
866
|
+
fi
|
|
867
|
+
elif [[ "${DCLAUDE_MOUNT_GH:-true}" == "true" ]]; then
|
|
868
|
+
debug "GitHub CLI config not found: ${HOME}/.config/gh"
|
|
869
|
+
fi
|
|
870
|
+
|
|
871
|
+
# Git configuration (git installed)
|
|
872
|
+
# Default to true when master switch is enabled
|
|
873
|
+
if [[ "${DCLAUDE_MOUNT_GIT:-true}" == "true" ]] && [[ -f "${HOME}/.gitconfig" ]]; then
|
|
874
|
+
if [[ -r "${HOME}/.gitconfig" ]]; then
|
|
875
|
+
docker_args+=(-v "${HOME}/.gitconfig:/home/claude/.gitconfig:ro")
|
|
876
|
+
debug "Mounting Git config: ${HOME}/.gitconfig"
|
|
877
|
+
((mounted_count++))
|
|
878
|
+
else
|
|
879
|
+
warning "Git config exists but is not readable: ${HOME}/.gitconfig"
|
|
880
|
+
fi
|
|
881
|
+
elif [[ "${DCLAUDE_MOUNT_GIT:-true}" == "true" ]]; then
|
|
882
|
+
debug "Git config not found: ${HOME}/.gitconfig"
|
|
883
|
+
fi
|
|
884
|
+
|
|
885
|
+
# NPM configuration (npm installed)
|
|
886
|
+
# Default to true when master switch is enabled
|
|
887
|
+
if [[ "${DCLAUDE_MOUNT_NPM:-true}" == "true" ]] && [[ -f "${HOME}/.npmrc" ]]; then
|
|
888
|
+
if [[ -r "${HOME}/.npmrc" ]]; then
|
|
889
|
+
docker_args+=(-v "${HOME}/.npmrc:/home/claude/.npmrc:ro")
|
|
890
|
+
debug "Mounting NPM config: ${HOME}/.npmrc"
|
|
891
|
+
((mounted_count++))
|
|
892
|
+
else
|
|
893
|
+
warning "NPM config exists but is not readable: ${HOME}/.npmrc"
|
|
894
|
+
fi
|
|
895
|
+
elif [[ "${DCLAUDE_MOUNT_NPM:-true}" == "true" ]]; then
|
|
896
|
+
debug "NPM config not found: ${HOME}/.npmrc"
|
|
897
|
+
fi
|
|
898
|
+
|
|
899
|
+
# Note: AWS CLI, gcloud, kubectl are NOT installed in the container
|
|
900
|
+
# These configs will not be mounted unless those tools are added to Dockerfile
|
|
901
|
+
|
|
902
|
+
# Security warning with details
|
|
903
|
+
if [[ $mounted_count -gt 0 ]]; then
|
|
904
|
+
warning "Mounted $mounted_count configuration directories (read-only)"
|
|
905
|
+
warning "Security: The following sensitive data is now accessible in the container:"
|
|
906
|
+
# SSH is now handled separately, don't warn here
|
|
907
|
+
if [[ "${DCLAUDE_MOUNT_DOCKER:-true}" == "true" ]] && [[ -d "${HOME}/.docker" ]] && [[ -r "${HOME}/.docker" ]]; then
|
|
908
|
+
warning " - Docker registry authentication tokens"
|
|
909
|
+
fi
|
|
910
|
+
if [[ "${DCLAUDE_MOUNT_GH:-true}" == "true" ]] && [[ -d "${HOME}/.config/gh" ]] && [[ -r "${HOME}/.config/gh" ]]; then
|
|
911
|
+
warning " - GitHub CLI authentication tokens"
|
|
912
|
+
fi
|
|
913
|
+
if [[ "${DCLAUDE_MOUNT_NPM:-true}" == "true" ]] && [[ -f "${HOME}/.npmrc" ]] && [[ -r "${HOME}/.npmrc" ]]; then
|
|
914
|
+
warning " - NPM registry authentication tokens"
|
|
915
|
+
fi
|
|
916
|
+
warning "Only use in trusted environments!"
|
|
917
|
+
fi
|
|
918
|
+
|
|
919
|
+
# Print arguments separated by null characters for safe parsing (only if we have args)
|
|
920
|
+
if [[ ${#docker_args[@]} -gt 0 ]]; then
|
|
921
|
+
printf '%s\0' "${docker_args[@]}"
|
|
922
|
+
fi
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
# Detect TTY availability and return appropriate Docker flags
|
|
926
|
+
detect_tty_flags() {
|
|
927
|
+
local tty_flags=""
|
|
928
|
+
|
|
929
|
+
# Check if stdin is a TTY
|
|
930
|
+
if [[ -t 0 ]]; then
|
|
931
|
+
tty_flags="-i"
|
|
932
|
+
fi
|
|
933
|
+
|
|
934
|
+
# Check if stdout is a TTY
|
|
935
|
+
if [[ -t 1 ]]; then
|
|
936
|
+
tty_flags="${tty_flags} -t"
|
|
937
|
+
fi
|
|
938
|
+
|
|
939
|
+
# Trim whitespace
|
|
940
|
+
tty_flags=$(echo $tty_flags | xargs)
|
|
941
|
+
|
|
942
|
+
if [[ "$DEBUG" == "true" ]]; then
|
|
943
|
+
debug "TTY detection: stdin=$(test -t 0 && echo yes || echo no), stdout=$(test -t 1 && echo yes || echo no)"
|
|
944
|
+
debug "TTY flags: ${tty_flags:-none}"
|
|
945
|
+
fi
|
|
946
|
+
|
|
947
|
+
echo "$tty_flags"
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
# Check if Claude is being run in print mode (-p/--print)
|
|
951
|
+
# This checks arguments as separate array elements, so -p inside a string won't match
|
|
952
|
+
is_print_mode() {
|
|
953
|
+
for arg in "$@"; do
|
|
954
|
+
if [[ "$arg" == "-p" ]] || [[ "$arg" == "--print" ]]; then
|
|
955
|
+
return 0
|
|
956
|
+
fi
|
|
957
|
+
done
|
|
958
|
+
return 1
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
# Main execution
|
|
962
|
+
main() {
|
|
963
|
+
# No argument parsing - pass everything through to Claude
|
|
964
|
+
# All arguments are preserved as-is for Claude
|
|
965
|
+
|
|
966
|
+
info "Verifying environment..."
|
|
967
|
+
debug "Host path: $HOST_PATH"
|
|
968
|
+
|
|
969
|
+
# Check prerequisites
|
|
970
|
+
check_docker
|
|
971
|
+
check_docker_running
|
|
972
|
+
|
|
973
|
+
# Setup environment
|
|
974
|
+
create_volumes
|
|
975
|
+
update_image
|
|
976
|
+
|
|
977
|
+
# Platform-specific settings
|
|
978
|
+
local platform
|
|
979
|
+
platform=$(detect_platform)
|
|
980
|
+
debug "Platform detected: $platform"
|
|
981
|
+
|
|
982
|
+
# Set network mode based on auto-detection or user preference
|
|
983
|
+
local network_mode="${DCLAUDE_NETWORK:-auto}"
|
|
984
|
+
local detection_source="default"
|
|
985
|
+
|
|
986
|
+
if [[ "$network_mode" == "auto" || -z "$network_mode" ]]; then
|
|
987
|
+
# Auto-detect network capability
|
|
988
|
+
network_mode=$(detect_network_capability)
|
|
989
|
+
detection_source="auto-detected"
|
|
990
|
+
debug "Auto-detected network mode: $network_mode"
|
|
991
|
+
|
|
992
|
+
# Show warning if using bridge mode
|
|
993
|
+
if [[ "$network_mode" == "bridge" ]]; then
|
|
994
|
+
warning "Using bridge networking mode on $platform"
|
|
995
|
+
info "Bridge mode limitations:"
|
|
996
|
+
info " - Cannot access services on localhost (use host.docker.internal instead)"
|
|
997
|
+
info " - Cannot access other containers via localhost"
|
|
998
|
+
info " - Port mapping required for container services"
|
|
999
|
+
info ""
|
|
1000
|
+
info "For better localhost access, consider:"
|
|
1001
|
+
info " - macOS: Enable host networking in Docker Desktop (beta) or use OrbStack"
|
|
1002
|
+
info " - Windows: Enable host networking in Docker Desktop (beta feature)"
|
|
1003
|
+
else
|
|
1004
|
+
debug "Host networking available - full localhost access enabled"
|
|
1005
|
+
fi
|
|
1006
|
+
elif [[ "$network_mode" =~ ^(host|bridge)$ ]]; then
|
|
1007
|
+
detection_source="user-specified"
|
|
1008
|
+
debug "Using user-specified network mode: $network_mode"
|
|
1009
|
+
else
|
|
1010
|
+
warning "Invalid network mode '$network_mode'. Valid options: host, bridge, auto. Falling back to auto-detection"
|
|
1011
|
+
network_mode=$(detect_network_capability)
|
|
1012
|
+
detection_source="fallback auto-detected"
|
|
1013
|
+
debug "Fallback auto-detected network mode: $network_mode"
|
|
1014
|
+
fi
|
|
1015
|
+
debug "Network mode: $network_mode ($detection_source)"
|
|
1016
|
+
|
|
1017
|
+
# Enhanced debug output for network configuration
|
|
1018
|
+
if [[ "$DEBUG" == "true" ]]; then
|
|
1019
|
+
debug "Network configuration summary:"
|
|
1020
|
+
debug " - Platform: $platform"
|
|
1021
|
+
debug " - Network mode: $network_mode"
|
|
1022
|
+
debug " - Detection source: $detection_source"
|
|
1023
|
+
debug " - DCLAUDE_NETWORK environment: ${DCLAUDE_NETWORK:-<not set>}"
|
|
1024
|
+
if [[ "$detection_source" =~ auto-detected ]]; then
|
|
1025
|
+
debug " - Auto-detection performed: yes"
|
|
1026
|
+
if [[ -f "${HOME}/.dclaude/network-mode" ]]; then
|
|
1027
|
+
local cache_age
|
|
1028
|
+
if cache_age=$(stat -c %Y "${HOME}/.dclaude/network-mode" 2>/dev/null) || cache_age=$(stat -f %m "${HOME}/.dclaude/network-mode" 2>/dev/null); then
|
|
1029
|
+
local current_time
|
|
1030
|
+
current_time=$(date +%s)
|
|
1031
|
+
local age_hours=$(( (current_time - cache_age) / 3600 ))
|
|
1032
|
+
debug " - Cache used: yes (age: ${age_hours}h)"
|
|
1033
|
+
else
|
|
1034
|
+
debug " - Cache used: yes (age: unknown)"
|
|
1035
|
+
fi
|
|
1036
|
+
else
|
|
1037
|
+
debug " - Cache used: no"
|
|
1038
|
+
fi
|
|
1039
|
+
else
|
|
1040
|
+
debug " - Auto-detection performed: no"
|
|
1041
|
+
fi
|
|
1042
|
+
fi
|
|
1043
|
+
|
|
1044
|
+
# Detect TTY availability
|
|
1045
|
+
local tty_flags=$(detect_tty_flags)
|
|
1046
|
+
|
|
1047
|
+
# Detect Docker socket early (needed for system context generation)
|
|
1048
|
+
if [[ -z "$DOCKER_SOCKET" ]]; then
|
|
1049
|
+
DOCKER_SOCKET=$(detect_docker_socket) || true
|
|
1050
|
+
fi
|
|
1051
|
+
debug "Docker socket detection: ${DOCKER_SOCKET:-<not found>}"
|
|
1052
|
+
|
|
1053
|
+
# Resolve git auth mode from 'auto' to actual mode (needed for system context)
|
|
1054
|
+
local resolved_git_auth="$GIT_AUTH_MODE"
|
|
1055
|
+
if [[ "$GIT_AUTH_MODE" == "auto" ]]; then
|
|
1056
|
+
# Same auto-detection logic as handle_git_auth()
|
|
1057
|
+
if [[ "$MOUNT_CONFIGS" == "true" ]] && [[ -d "${HOME}/.ssh" ]] && [[ -r "${HOME}/.ssh" ]]; then
|
|
1058
|
+
resolved_git_auth="key-mount"
|
|
1059
|
+
debug "Git auth resolved: key-mount (DCLAUDE_MOUNT_CONFIGS=true)"
|
|
1060
|
+
elif [[ -n "${SSH_AUTH_SOCK:-}" ]] && [[ -S "${SSH_AUTH_SOCK}" ]]; then
|
|
1061
|
+
resolved_git_auth="agent-forwarding"
|
|
1062
|
+
debug "Git auth resolved: agent-forwarding (active agent detected)"
|
|
1063
|
+
elif [[ -d "${HOME}/.ssh" ]] && [[ -r "${HOME}/.ssh" ]]; then
|
|
1064
|
+
resolved_git_auth="key-mount"
|
|
1065
|
+
debug "Git auth resolved: key-mount (SSH directory detected)"
|
|
1066
|
+
else
|
|
1067
|
+
resolved_git_auth="none"
|
|
1068
|
+
debug "Git auth resolved: none (no SSH agent or keys found)"
|
|
1069
|
+
fi
|
|
1070
|
+
else
|
|
1071
|
+
debug "Git auth: $resolved_git_auth (user-specified)"
|
|
1072
|
+
fi
|
|
1073
|
+
|
|
1074
|
+
# Generate system context for Claude (if enabled) - must be done early for all code paths
|
|
1075
|
+
local claude_args=()
|
|
1076
|
+
if [[ "$ENABLE_SYSTEM_CONTEXT" == "true" ]]; then
|
|
1077
|
+
local has_docker="false"
|
|
1078
|
+
[[ -n "$DOCKER_SOCKET" ]] && [[ -S "$DOCKER_SOCKET" ]] && has_docker="true"
|
|
1079
|
+
|
|
1080
|
+
local system_context
|
|
1081
|
+
system_context=$(generate_system_context "$network_mode" "$resolved_git_auth" "$has_docker" "$platform" "$DOCKER_SOCKET")
|
|
1082
|
+
|
|
1083
|
+
claude_args+=("--append-system-prompt" "$system_context")
|
|
1084
|
+
debug "System context enabled (${#system_context} chars, has_docker=$has_docker, git_auth=$resolved_git_auth, platform=$platform)"
|
|
1085
|
+
if [[ "$DEBUG" == "true" ]]; then
|
|
1086
|
+
debug "System context preview: ${system_context:0:100}..."
|
|
1087
|
+
fi
|
|
1088
|
+
else
|
|
1089
|
+
debug "System context disabled (DCLAUDE_SYSTEM_CONTEXT=$ENABLE_SYSTEM_CONTEXT)"
|
|
1090
|
+
fi
|
|
1091
|
+
|
|
1092
|
+
# Generate container name based on path (for reuse when DCLAUDE_RM=false)
|
|
1093
|
+
local container_name=""
|
|
1094
|
+
if [[ "$REMOVE_CONTAINER" == "false" ]]; then
|
|
1095
|
+
# Create deterministic name from path hash
|
|
1096
|
+
container_name=$(get_container_name "$HOST_PATH")
|
|
1097
|
+
debug "Container name: $container_name (path: $HOST_PATH)"
|
|
1098
|
+
|
|
1099
|
+
# Check if container already exists
|
|
1100
|
+
if docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then
|
|
1101
|
+
local container_status=$(docker inspect --format='{{.State.Status}}' "$container_name" 2>/dev/null)
|
|
1102
|
+
debug "Found existing container: $container_name (status: $container_status)"
|
|
1103
|
+
|
|
1104
|
+
if [[ "$container_status" == "running" ]]; then
|
|
1105
|
+
info "Attaching to running container: $container_name"
|
|
1106
|
+
elif [[ "$container_status" == "exited" ]]; then
|
|
1107
|
+
info "Restarting existing container: $container_name"
|
|
1108
|
+
docker start "$container_name" >/dev/null
|
|
1109
|
+
|
|
1110
|
+
# Wait for container to be running
|
|
1111
|
+
local wait_count=0
|
|
1112
|
+
while [[ $wait_count -lt 30 ]]; do
|
|
1113
|
+
if docker ps -q -f name="^${container_name}$" 2>/dev/null | grep -q .; then
|
|
1114
|
+
debug "Container restarted successfully"
|
|
1115
|
+
break
|
|
1116
|
+
fi
|
|
1117
|
+
sleep 0.1
|
|
1118
|
+
((wait_count++))
|
|
1119
|
+
done
|
|
1120
|
+
fi
|
|
1121
|
+
|
|
1122
|
+
# Check if interactive (TTY available) and not in print mode
|
|
1123
|
+
if [[ -n "$tty_flags" ]] && ! is_print_mode "${claude_args[@]}" "$@"; then
|
|
1124
|
+
# Interactive and not print mode - use tmux for session management
|
|
1125
|
+
local tmux_session
|
|
1126
|
+
if [[ -n "${DCLAUDE_TMUX_SESSION:-}" ]]; then
|
|
1127
|
+
tmux_session="$DCLAUDE_TMUX_SESSION"
|
|
1128
|
+
debug "Using custom tmux session name: $tmux_session"
|
|
1129
|
+
else
|
|
1130
|
+
tmux_session="claude-$(date +%Y%m%d-%H%M%S)"
|
|
1131
|
+
debug "Generated unique tmux session name: $tmux_session"
|
|
1132
|
+
fi
|
|
1133
|
+
|
|
1134
|
+
# Build env args for docker exec to pass terminal info to tmux session
|
|
1135
|
+
local exec_env_args=()
|
|
1136
|
+
[[ -n "${TERM_PROGRAM:-}" ]] && exec_env_args+=(-e "TERM_PROGRAM=${TERM_PROGRAM}")
|
|
1137
|
+
[[ -n "${TERM_PROGRAM_VERSION:-}" ]] && exec_env_args+=(-e "TERM_PROGRAM_VERSION=${TERM_PROGRAM_VERSION}")
|
|
1138
|
+
[[ -n "${TERM_SESSION_ID:-}" ]] && exec_env_args+=(-e "TERM_SESSION_ID=${TERM_SESSION_ID}")
|
|
1139
|
+
[[ -n "${COLORTERM:-}" ]] && exec_env_args+=(-e "COLORTERM=${COLORTERM}")
|
|
1140
|
+
# Internal env vars for tmux status bar display
|
|
1141
|
+
exec_env_args+=(-e "_DCLAUDE_NET=${network_mode}")
|
|
1142
|
+
exec_env_args+=(-e "_DCLAUDE_TAG=${DCLAUDE_TAG:-latest}")
|
|
1143
|
+
exec_env_args+=(-e "_DCLAUDE_SESSION=${DCLAUDE_TMUX_SESSION:-auto}")
|
|
1144
|
+
|
|
1145
|
+
debug "Creating new tmux session running Claude"
|
|
1146
|
+
debug "Claude args count: ${#claude_args[@]}, user args: $*"
|
|
1147
|
+
info "Starting new Claude session..."
|
|
1148
|
+
docker exec -it -u claude "${exec_env_args[@]}" "$container_name" tmux -f /home/claude/.tmux.conf new-session -s "$tmux_session" claude "${claude_args[@]}" "$@"
|
|
1149
|
+
local tmux_exit=$?
|
|
1150
|
+
reset_terminal_mouse
|
|
1151
|
+
exit $tmux_exit
|
|
1152
|
+
else
|
|
1153
|
+
# Non-interactive or print mode - run claude directly without tmux
|
|
1154
|
+
debug "Non-interactive or print mode, running Claude directly (no tmux)"
|
|
1155
|
+
debug "Claude args count: ${#claude_args[@]}, user args: $*"
|
|
1156
|
+
exec docker exec -u claude -w "$HOST_PATH" "$container_name" claude "${claude_args[@]}" "$@"
|
|
1157
|
+
fi
|
|
1158
|
+
fi
|
|
1159
|
+
fi
|
|
1160
|
+
|
|
1161
|
+
# Prepare Docker run arguments
|
|
1162
|
+
DOCKER_ARGS=(
|
|
1163
|
+
"run"
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
# Add --rm flag if enabled (default: true)
|
|
1167
|
+
if [[ "$REMOVE_CONTAINER" == "true" ]]; then
|
|
1168
|
+
DOCKER_ARGS+=("--rm")
|
|
1169
|
+
else
|
|
1170
|
+
# Use named container for reuse
|
|
1171
|
+
DOCKER_ARGS+=("--name" "$container_name")
|
|
1172
|
+
fi
|
|
1173
|
+
|
|
1174
|
+
# TTY flags will be added only when needed (ephemeral containers)
|
|
1175
|
+
# Persistent containers run in background and shouldn't have TTY/stdin attached
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
DOCKER_ARGS+=(
|
|
1179
|
+
# Mount current directory
|
|
1180
|
+
-v "${HOST_PATH}:${HOST_PATH}"
|
|
1181
|
+
# Mount persistent Claude configuration volume
|
|
1182
|
+
-v "${VOLUME_PREFIX}-claude:/home/claude/.claude"
|
|
1183
|
+
# Mount persistent config volume (gh, etc.)
|
|
1184
|
+
-v "${VOLUME_PREFIX}-config:/home/claude/.config"
|
|
1185
|
+
# Set working directory
|
|
1186
|
+
-w "${HOST_PATH}"
|
|
1187
|
+
# Network mode
|
|
1188
|
+
--network="$network_mode"
|
|
1189
|
+
# Environment - pass through terminal identification for proper feature detection
|
|
1190
|
+
-e "TERM=${TERM:-xterm-256color}"
|
|
1191
|
+
)
|
|
1192
|
+
|
|
1193
|
+
# Reserve SSH port for remote access (JetBrains Gateway, VS Code Remote, etc.)
|
|
1194
|
+
local ssh_port
|
|
1195
|
+
ssh_port=$(find_available_port 2222 65000)
|
|
1196
|
+
debug "SSH port reserved: $ssh_port"
|
|
1197
|
+
DOCKER_ARGS+=(--label "dclaude.ssh.port=${ssh_port}")
|
|
1198
|
+
|
|
1199
|
+
# Port mapping only needed for bridge mode (host mode shares network stack)
|
|
1200
|
+
if [[ "$network_mode" != "host" ]]; then
|
|
1201
|
+
DOCKER_ARGS+=(-p "${ssh_port}:${ssh_port}")
|
|
1202
|
+
debug "SSH port mapping: ${ssh_port}:${ssh_port} (bridge mode)"
|
|
1203
|
+
else
|
|
1204
|
+
debug "SSH port: ${ssh_port} (host mode, no mapping needed)"
|
|
1205
|
+
fi
|
|
1206
|
+
|
|
1207
|
+
# Pass through terminal program information for proper logo rendering
|
|
1208
|
+
if [[ -n "${TERM_PROGRAM:-}" ]]; then
|
|
1209
|
+
DOCKER_ARGS+=(-e "TERM_PROGRAM=${TERM_PROGRAM}")
|
|
1210
|
+
fi
|
|
1211
|
+
if [[ -n "${TERM_PROGRAM_VERSION:-}" ]]; then
|
|
1212
|
+
DOCKER_ARGS+=(-e "TERM_PROGRAM_VERSION=${TERM_PROGRAM_VERSION}")
|
|
1213
|
+
fi
|
|
1214
|
+
if [[ -n "${TERM_SESSION_ID:-}" ]]; then
|
|
1215
|
+
DOCKER_ARGS+=(-e "TERM_SESSION_ID=${TERM_SESSION_ID}")
|
|
1216
|
+
fi
|
|
1217
|
+
if [[ -n "${COLORTERM:-}" ]]; then
|
|
1218
|
+
DOCKER_ARGS+=(-e "COLORTERM=${COLORTERM}")
|
|
1219
|
+
fi
|
|
1220
|
+
|
|
1221
|
+
# Mount Docker socket if detected (detection done earlier for system context)
|
|
1222
|
+
if [[ -n "$DOCKER_SOCKET" ]] && [[ -S "$DOCKER_SOCKET" ]]; then
|
|
1223
|
+
DOCKER_ARGS+=(-v "${DOCKER_SOCKET}:/var/run/docker.sock")
|
|
1224
|
+
debug "Docker socket mounted: $DOCKER_SOCKET"
|
|
1225
|
+
else
|
|
1226
|
+
warning "Docker socket not found"
|
|
1227
|
+
warning "Container will not have Docker access"
|
|
1228
|
+
debug "Searched standard locations and Docker context"
|
|
1229
|
+
fi
|
|
1230
|
+
|
|
1231
|
+
# Handle SSH authentication (agent forwarding or key mounting)
|
|
1232
|
+
# Use process substitution to preserve null bytes
|
|
1233
|
+
while IFS= read -r -d '' ssh_arg; do
|
|
1234
|
+
[[ -n "$ssh_arg" ]] && DOCKER_ARGS+=("$ssh_arg")
|
|
1235
|
+
done < <(handle_git_auth)
|
|
1236
|
+
|
|
1237
|
+
# Mount host configurations if enabled
|
|
1238
|
+
# Use process substitution to preserve null bytes
|
|
1239
|
+
while IFS= read -r -d '' config_arg; do
|
|
1240
|
+
[[ -n "$config_arg" ]] && DOCKER_ARGS+=("$config_arg")
|
|
1241
|
+
done < <(mount_host_configs)
|
|
1242
|
+
|
|
1243
|
+
# Add any additional environment variables
|
|
1244
|
+
if [[ -n "${CLAUDE_MODEL:-}" ]]; then
|
|
1245
|
+
DOCKER_ARGS+=(-e "CLAUDE_MODEL=${CLAUDE_MODEL}")
|
|
1246
|
+
fi
|
|
1247
|
+
|
|
1248
|
+
if [[ "$DEBUG" == "true" ]]; then
|
|
1249
|
+
debug "Container removal: $REMOVE_CONTAINER (DCLAUDE_RM=${DCLAUDE_RM:-<not set>})"
|
|
1250
|
+
debug "Docker command: docker ${DOCKER_ARGS[*]} $IMAGE $*"
|
|
1251
|
+
fi
|
|
1252
|
+
|
|
1253
|
+
# Run Claude in Docker
|
|
1254
|
+
if [[ "$REMOVE_CONTAINER" == "false" ]]; then
|
|
1255
|
+
# For persistent containers, run tini with tail daemon for zombie reaping
|
|
1256
|
+
# tini will reap zombie processes created by exec commands
|
|
1257
|
+
debug "Starting persistent container with tini + tail daemon"
|
|
1258
|
+
|
|
1259
|
+
# TODO: Add retry logic for SSH port allocation race condition
|
|
1260
|
+
# When multiple dclaude instances start simultaneously, they can both detect
|
|
1261
|
+
# the same SSH port as available before either binds to it. This causes the
|
|
1262
|
+
# second instance to fail with "port is already allocated" error.
|
|
1263
|
+
# Solution: If docker run fails with port conflict, re-detect port and retry (max 3 attempts)
|
|
1264
|
+
# See: SSH_TEST_RESULTS.md - Issue 1: Race Condition in Parallel Container Startup
|
|
1265
|
+
|
|
1266
|
+
# Run in detached mode (-d) and capture output (Container ID) or errors
|
|
1267
|
+
local run_output
|
|
1268
|
+
if ! run_output=$(docker "${DOCKER_ARGS[@]}" -d --entrypoint /usr/bin/tini "$IMAGE" -- tail -f /dev/null 2>&1); then
|
|
1269
|
+
error "Failed to start container: $run_output"
|
|
1270
|
+
exit 1
|
|
1271
|
+
fi
|
|
1272
|
+
|
|
1273
|
+
debug "Container started successfully: $run_output"
|
|
1274
|
+
|
|
1275
|
+
# Run entrypoint setup as root (for Docker socket permissions, etc.)
|
|
1276
|
+
debug "Running entrypoint initialization"
|
|
1277
|
+
docker exec -u root "$container_name" /usr/local/bin/docker-entrypoint.sh true >/dev/null 2>&1 || true
|
|
1278
|
+
|
|
1279
|
+
# Check if interactive (TTY available) and not in print mode
|
|
1280
|
+
if [[ -n "$tty_flags" ]] && ! is_print_mode "${claude_args[@]}" "$@"; then
|
|
1281
|
+
# Interactive and not print mode - use tmux for session management
|
|
1282
|
+
local tmux_session
|
|
1283
|
+
if [[ -n "${DCLAUDE_TMUX_SESSION:-}" ]]; then
|
|
1284
|
+
tmux_session="$DCLAUDE_TMUX_SESSION"
|
|
1285
|
+
debug "Using custom tmux session name: $tmux_session"
|
|
1286
|
+
else
|
|
1287
|
+
tmux_session="claude-$(date +%Y%m%d-%H%M%S)"
|
|
1288
|
+
debug "Generated unique tmux session name: $tmux_session"
|
|
1289
|
+
fi
|
|
1290
|
+
|
|
1291
|
+
# Build env args for docker exec to pass terminal info to tmux session
|
|
1292
|
+
local exec_env_args=()
|
|
1293
|
+
[[ -n "${TERM_PROGRAM:-}" ]] && exec_env_args+=(-e "TERM_PROGRAM=${TERM_PROGRAM}")
|
|
1294
|
+
[[ -n "${TERM_PROGRAM_VERSION:-}" ]] && exec_env_args+=(-e "TERM_PROGRAM_VERSION=${TERM_PROGRAM_VERSION}")
|
|
1295
|
+
[[ -n "${TERM_SESSION_ID:-}" ]] && exec_env_args+=(-e "TERM_SESSION_ID=${TERM_SESSION_ID}")
|
|
1296
|
+
[[ -n "${COLORTERM:-}" ]] && exec_env_args+=(-e "COLORTERM=${COLORTERM}")
|
|
1297
|
+
# Internal env vars for tmux status bar display
|
|
1298
|
+
exec_env_args+=(-e "_DCLAUDE_NET=${network_mode}")
|
|
1299
|
+
exec_env_args+=(-e "_DCLAUDE_TAG=${DCLAUDE_TAG:-latest}")
|
|
1300
|
+
exec_env_args+=(-e "_DCLAUDE_SESSION=${DCLAUDE_TMUX_SESSION:-auto}")
|
|
1301
|
+
|
|
1302
|
+
debug "Creating new tmux session running Claude"
|
|
1303
|
+
debug "Claude args count: ${#claude_args[@]}, user args: $*"
|
|
1304
|
+
info "Starting new Claude session..."
|
|
1305
|
+
docker exec -it -u claude "${exec_env_args[@]}" "$container_name" tmux -f /home/claude/.tmux.conf new-session -s "$tmux_session" claude "${claude_args[@]}" "$@"
|
|
1306
|
+
local tmux_exit=$?
|
|
1307
|
+
reset_terminal_mouse
|
|
1308
|
+
exit $tmux_exit
|
|
1309
|
+
else
|
|
1310
|
+
# Non-interactive or print mode - run claude directly without tmux
|
|
1311
|
+
debug "Non-interactive or print mode, running Claude directly (no tmux)"
|
|
1312
|
+
debug "Claude args count: ${#claude_args[@]}, user args: $*"
|
|
1313
|
+
exec docker exec -u claude -w "$HOST_PATH" "$container_name" claude "${claude_args[@]}" "$@"
|
|
1314
|
+
fi
|
|
1315
|
+
else
|
|
1316
|
+
# Ephemeral container - run directly
|
|
1317
|
+
# Add TTY flags here as we are running interactively
|
|
1318
|
+
if [[ -n "$tty_flags" ]]; then
|
|
1319
|
+
DOCKER_ARGS+=($tty_flags)
|
|
1320
|
+
fi
|
|
1321
|
+
debug "Claude args count: ${#claude_args[@]}, user args: $*"
|
|
1322
|
+
exec docker "${DOCKER_ARGS[@]}" "$IMAGE" "${claude_args[@]}" "$@"
|
|
1323
|
+
fi
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
# Subcommand: exec into container
|
|
1329
|
+
cmd_exec() {
|
|
1330
|
+
# Generate container name from path hash (same logic as main)
|
|
1331
|
+
local container_name=$(get_container_name "$HOST_PATH")
|
|
1332
|
+
|
|
1333
|
+
|
|
1334
|
+
debug "Looking for container: $container_name (path: $HOST_PATH)"
|
|
1335
|
+
|
|
1336
|
+
# Check if container exists
|
|
1337
|
+
if ! docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then
|
|
1338
|
+
error "No container found for this directory"
|
|
1339
|
+
info "Run 'DCLAUDE_RM=false dclaude' first to create a persistent container"
|
|
1340
|
+
exit 1
|
|
1341
|
+
fi
|
|
1342
|
+
|
|
1343
|
+
local container_status=$(docker inspect --format='{{.State.Status}}' "$container_name" 2>/dev/null)
|
|
1344
|
+
debug "Container status: $container_status"
|
|
1345
|
+
|
|
1346
|
+
if [[ "$container_status" != "running" ]]; then
|
|
1347
|
+
if [[ "$container_status" == "exited" ]]; then
|
|
1348
|
+
info "Restarting existing container: $container_name"
|
|
1349
|
+
docker start "$container_name" >/dev/null
|
|
1350
|
+
debug "Container restarted successfully"
|
|
1351
|
+
else
|
|
1352
|
+
error "Container $container_name is in unexpected state: $container_status"
|
|
1353
|
+
info "Remove it with: docker rm $container_name"
|
|
1354
|
+
exit 1
|
|
1355
|
+
fi
|
|
1356
|
+
fi
|
|
1357
|
+
|
|
1358
|
+
# Detect TTY availability
|
|
1359
|
+
local tty_flags=$(detect_tty_flags)
|
|
1360
|
+
|
|
1361
|
+
# Exec into container as claude user
|
|
1362
|
+
if [[ $# -eq 0 ]]; then
|
|
1363
|
+
# No command specified, open bash shell
|
|
1364
|
+
info "Opening shell in container: $container_name"
|
|
1365
|
+
debug "Exec command: docker exec $tty_flags -u claude -w $HOST_PATH $container_name bash"
|
|
1366
|
+
exec docker exec $tty_flags -u claude -w "$HOST_PATH" "$container_name" bash
|
|
1367
|
+
else
|
|
1368
|
+
# Execute specific command
|
|
1369
|
+
info "Executing command in container: $container_name"
|
|
1370
|
+
debug "Exec command: docker exec $tty_flags -u claude -w $HOST_PATH $container_name $*"
|
|
1371
|
+
exec docker exec $tty_flags -u claude -w "$HOST_PATH" "$container_name" "$@"
|
|
1372
|
+
fi
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
# Subcommand: authenticate GitHub CLI
|
|
1376
|
+
cmd_gh() {
|
|
1377
|
+
# Generate container name based on current directory
|
|
1378
|
+
local container_name=$(get_container_name "$HOST_PATH")
|
|
1379
|
+
|
|
1380
|
+
# Check if container exists
|
|
1381
|
+
if ! docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then
|
|
1382
|
+
error "No container found for this directory"
|
|
1383
|
+
info "Run 'dclaude' first to create a container"
|
|
1384
|
+
exit 1
|
|
1385
|
+
fi
|
|
1386
|
+
|
|
1387
|
+
# Check if container is running
|
|
1388
|
+
local container_status=$(docker inspect --format='{{.State.Status}}' "$container_name" 2>/dev/null)
|
|
1389
|
+
if [[ "$container_status" != "running" ]]; then
|
|
1390
|
+
if [[ "$container_status" == "exited" ]]; then
|
|
1391
|
+
info "Restarting container: $container_name"
|
|
1392
|
+
docker start "$container_name" >/dev/null
|
|
1393
|
+
else
|
|
1394
|
+
error "Container $container_name is in unexpected state: $container_status"
|
|
1395
|
+
exit 1
|
|
1396
|
+
fi
|
|
1397
|
+
fi
|
|
1398
|
+
|
|
1399
|
+
info "Starting GitHub CLI authentication..."
|
|
1400
|
+
exec docker exec -it -u claude "$container_name" gh auth login
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
# Subcommand: attach to existing tmux session
|
|
1404
|
+
cmd_attach() {
|
|
1405
|
+
local session_name="${1:-${DCLAUDE_TMUX_SESSION:-}}"
|
|
1406
|
+
|
|
1407
|
+
if [[ -z "$session_name" ]]; then
|
|
1408
|
+
error "No session name provided"
|
|
1409
|
+
info "Usage: dclaude attach <session-name>"
|
|
1410
|
+
info " or: DCLAUDE_TMUX_SESSION=<name> dclaude attach"
|
|
1411
|
+
exit 1
|
|
1412
|
+
fi
|
|
1413
|
+
|
|
1414
|
+
# Generate container name based on current directory
|
|
1415
|
+
local container_name=$(get_container_name "$HOST_PATH")
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
# Check if container exists
|
|
1419
|
+
if ! docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then
|
|
1420
|
+
error "No container found for this directory"
|
|
1421
|
+
info "Run 'dclaude' first to create a container"
|
|
1422
|
+
exit 1
|
|
1423
|
+
fi
|
|
1424
|
+
|
|
1425
|
+
# Check if container is running
|
|
1426
|
+
local container_status=$(docker inspect --format='{{.State.Status}}' "$container_name" 2>/dev/null)
|
|
1427
|
+
if [[ "$container_status" != "running" ]]; then
|
|
1428
|
+
if [[ "$container_status" == "exited" ]]; then
|
|
1429
|
+
info "Restarting container: $container_name"
|
|
1430
|
+
docker start "$container_name" >/dev/null
|
|
1431
|
+
else
|
|
1432
|
+
error "Container $container_name is in unexpected state: $container_status"
|
|
1433
|
+
exit 1
|
|
1434
|
+
fi
|
|
1435
|
+
fi
|
|
1436
|
+
|
|
1437
|
+
# Check if session exists
|
|
1438
|
+
if ! docker exec -u claude "$container_name" tmux has-session -t "$session_name" 2>/dev/null; then
|
|
1439
|
+
error "Session '$session_name' not found in container"
|
|
1440
|
+
info "Available sessions:"
|
|
1441
|
+
docker exec -u claude "$container_name" tmux list-sessions 2>/dev/null || echo " (no sessions running)"
|
|
1442
|
+
exit 1
|
|
1443
|
+
fi
|
|
1444
|
+
|
|
1445
|
+
# Build env args for docker exec
|
|
1446
|
+
local exec_env_args=()
|
|
1447
|
+
[[ -n "${TERM_PROGRAM:-}" ]] && exec_env_args+=(-e "TERM_PROGRAM=${TERM_PROGRAM}")
|
|
1448
|
+
[[ -n "${TERM_PROGRAM_VERSION:-}" ]] && exec_env_args+=(-e "TERM_PROGRAM_VERSION=${TERM_PROGRAM_VERSION}")
|
|
1449
|
+
[[ -n "${TERM_SESSION_ID:-}" ]] && exec_env_args+=(-e "TERM_SESSION_ID=${TERM_SESSION_ID}")
|
|
1450
|
+
[[ -n "${COLORTERM:-}" ]] && exec_env_args+=(-e "COLORTERM=${COLORTERM}")
|
|
1451
|
+
|
|
1452
|
+
# Attach to existing session
|
|
1453
|
+
info "Attaching to session: $session_name"
|
|
1454
|
+
docker exec -it -u claude "${exec_env_args[@]}" "$container_name" tmux -f /home/claude/.tmux.conf attach-session -t "$session_name"
|
|
1455
|
+
local tmux_exit=$?
|
|
1456
|
+
reset_terminal_mouse
|
|
1457
|
+
exit $tmux_exit
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
# Subcommand: launch Chrome with DevTools and ensure MCP configured
|
|
1461
|
+
cmd_chrome() {
|
|
1462
|
+
local setup_only=false
|
|
1463
|
+
|
|
1464
|
+
# Parse flags
|
|
1465
|
+
while [[ $# -gt 0 ]]; do
|
|
1466
|
+
case "$1" in
|
|
1467
|
+
--setup-only)
|
|
1468
|
+
setup_only=true
|
|
1469
|
+
shift
|
|
1470
|
+
;;
|
|
1471
|
+
--port=*)
|
|
1472
|
+
CHROME_PORT="${1#*=}"
|
|
1473
|
+
shift
|
|
1474
|
+
;;
|
|
1475
|
+
*)
|
|
1476
|
+
error "Unknown option: $1"
|
|
1477
|
+
info "Usage: dclaude chrome [--setup-only] [--port=PORT]"
|
|
1478
|
+
exit 1
|
|
1479
|
+
;;
|
|
1480
|
+
esac
|
|
1481
|
+
done
|
|
1482
|
+
|
|
1483
|
+
info "Setting up Chrome DevTools integration"
|
|
1484
|
+
|
|
1485
|
+
# 1. Detect Chrome binary
|
|
1486
|
+
local chrome_bin="$CHROME_BIN"
|
|
1487
|
+
if [[ -z "$chrome_bin" ]]; then
|
|
1488
|
+
debug "Auto-detecting Chrome binary"
|
|
1489
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
1490
|
+
# macOS
|
|
1491
|
+
if [[ -f "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" ]]; then
|
|
1492
|
+
chrome_bin="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
|
|
1493
|
+
elif [[ -f "/Applications/Chromium.app/Contents/MacOS/Chromium" ]]; then
|
|
1494
|
+
chrome_bin="/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
1495
|
+
fi
|
|
1496
|
+
elif [[ "$(uname)" == "Linux" ]]; then
|
|
1497
|
+
# Linux
|
|
1498
|
+
chrome_bin=$(command -v google-chrome || command -v chromium-browser || command -v chromium || echo "")
|
|
1499
|
+
fi
|
|
1500
|
+
|
|
1501
|
+
if [[ -z "$chrome_bin" ]]; then
|
|
1502
|
+
error "Chrome not found. Set DCLAUDE_CHROME_BIN to specify location"
|
|
1503
|
+
exit 1
|
|
1504
|
+
fi
|
|
1505
|
+
debug "Found Chrome: $chrome_bin"
|
|
1506
|
+
fi
|
|
1507
|
+
|
|
1508
|
+
# 2. Setup profile directory
|
|
1509
|
+
local profile_dir="$HOST_PATH/.dclaude/chrome/profiles/$CHROME_PROFILE"
|
|
1510
|
+
mkdir -p "$profile_dir"
|
|
1511
|
+
debug "Profile directory: $profile_dir"
|
|
1512
|
+
|
|
1513
|
+
# 3. Check/create .mcp.json
|
|
1514
|
+
local mcp_json="$HOST_PATH/.mcp.json"
|
|
1515
|
+
local mcp_port=""
|
|
1516
|
+
local port_mismatch=false
|
|
1517
|
+
|
|
1518
|
+
if [[ -f "$mcp_json" ]]; then
|
|
1519
|
+
debug "Found existing .mcp.json"
|
|
1520
|
+
# Extract port from --browserUrl in .mcp.json
|
|
1521
|
+
mcp_port=$(jq -r '.mcpServers.chrome.args[]? | select(startswith("--browserUrl=")) | split(":")[-1]' "$mcp_json" 2>/dev/null || echo "")
|
|
1522
|
+
|
|
1523
|
+
if [[ -n "$mcp_port" && "$mcp_port" != "$CHROME_PORT" ]]; then
|
|
1524
|
+
port_mismatch=true
|
|
1525
|
+
fi
|
|
1526
|
+
else
|
|
1527
|
+
debug "Creating .mcp.json"
|
|
1528
|
+
cat > "$mcp_json" << EOF
|
|
1529
|
+
{
|
|
1530
|
+
"mcpServers": {
|
|
1531
|
+
"chrome": {
|
|
1532
|
+
"command": "npx",
|
|
1533
|
+
"args": [
|
|
1534
|
+
"-y",
|
|
1535
|
+
"chrome-devtools-mcp@latest",
|
|
1536
|
+
"--browserUrl=http://localhost:${CHROME_PORT}"
|
|
1537
|
+
]
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
EOF
|
|
1542
|
+
success "Created .mcp.json with Chrome MCP server (port ${CHROME_PORT})"
|
|
1543
|
+
fi
|
|
1544
|
+
|
|
1545
|
+
# 4. Warn if port mismatch
|
|
1546
|
+
if [[ "$port_mismatch" == "true" ]]; then
|
|
1547
|
+
warning "Port mismatch detected!"
|
|
1548
|
+
echo ""
|
|
1549
|
+
echo " Chrome will launch on port: ${CHROME_PORT}"
|
|
1550
|
+
echo " MCP expects (.mcp.json): ${mcp_port}"
|
|
1551
|
+
echo ""
|
|
1552
|
+
warning "MCP will not be able to connect until .mcp.json is updated"
|
|
1553
|
+
echo ""
|
|
1554
|
+
fi
|
|
1555
|
+
|
|
1556
|
+
# 5. Exit if setup-only
|
|
1557
|
+
if [[ "$setup_only" == "true" ]]; then
|
|
1558
|
+
success "Setup complete (--setup-only mode)"
|
|
1559
|
+
exit 0
|
|
1560
|
+
fi
|
|
1561
|
+
|
|
1562
|
+
# 6. Check if Chrome already running on this port
|
|
1563
|
+
if curl -s "http://localhost:${CHROME_PORT}/json/version" >/dev/null 2>&1; then
|
|
1564
|
+
success "Chrome already running on port ${CHROME_PORT}"
|
|
1565
|
+
curl -s "http://localhost:${CHROME_PORT}/json/version" | jq -r '" Browser: " + .Browser'
|
|
1566
|
+
exit 0
|
|
1567
|
+
fi
|
|
1568
|
+
|
|
1569
|
+
# 7. Launch Chrome
|
|
1570
|
+
info "Launching Chrome with remote debugging on port ${CHROME_PORT}"
|
|
1571
|
+
|
|
1572
|
+
local chrome_args=(
|
|
1573
|
+
"--user-data-dir=$profile_dir"
|
|
1574
|
+
"--remote-debugging-port=$CHROME_PORT"
|
|
1575
|
+
"--no-first-run"
|
|
1576
|
+
"--no-default-browser-check"
|
|
1577
|
+
"--disable-default-apps"
|
|
1578
|
+
"--disable-sync"
|
|
1579
|
+
"--allow-insecure-localhost"
|
|
1580
|
+
)
|
|
1581
|
+
|
|
1582
|
+
# Add user-specified flags
|
|
1583
|
+
if [[ -n "$CHROME_FLAGS" ]]; then
|
|
1584
|
+
debug "Adding custom flags: $CHROME_FLAGS"
|
|
1585
|
+
read -ra custom_flags <<< "$CHROME_FLAGS"
|
|
1586
|
+
chrome_args+=("${custom_flags[@]}")
|
|
1587
|
+
fi
|
|
1588
|
+
|
|
1589
|
+
debug "Chrome command: $chrome_bin ${chrome_args[*]}"
|
|
1590
|
+
|
|
1591
|
+
# Launch Chrome in background
|
|
1592
|
+
"$chrome_bin" "${chrome_args[@]}" >/dev/null 2>&1 &
|
|
1593
|
+
local chrome_pid=$!
|
|
1594
|
+
|
|
1595
|
+
# 8. Wait for Chrome to be ready
|
|
1596
|
+
info "Waiting for Chrome to start..."
|
|
1597
|
+
local wait_count=0
|
|
1598
|
+
while [[ $wait_count -lt 30 ]]; do
|
|
1599
|
+
if curl -s "http://localhost:${CHROME_PORT}/json/version" >/dev/null 2>&1; then
|
|
1600
|
+
success "Chrome is ready on port ${CHROME_PORT}"
|
|
1601
|
+
curl -s "http://localhost:${CHROME_PORT}/json/version" | jq -r '" Browser: " + .Browser + "\n Protocol: " + ."Protocol-Version" + "\n WebSocket: " + .webSocketDebuggerUrl'
|
|
1602
|
+
echo ""
|
|
1603
|
+
success "Chrome DevTools ready for MCP integration!"
|
|
1604
|
+
info "Next step: Run 'dclaude' to start Claude with Chrome MCP"
|
|
1605
|
+
exit 0
|
|
1606
|
+
fi
|
|
1607
|
+
sleep 0.5
|
|
1608
|
+
((wait_count++))
|
|
1609
|
+
done
|
|
1610
|
+
|
|
1611
|
+
error "Chrome failed to start or remote debugging port not accessible"
|
|
1612
|
+
exit 1
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
# Subcommand: pull latest image
|
|
1616
|
+
cmd_pull() {
|
|
1617
|
+
info "Pulling latest image: $IMAGE"
|
|
1618
|
+
if docker pull "$IMAGE"; then
|
|
1619
|
+
success "Image pulled successfully"
|
|
1620
|
+
else
|
|
1621
|
+
error "Failed to pull image: $IMAGE"
|
|
1622
|
+
exit 1
|
|
1623
|
+
fi
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
# Subcommand: update Claude CLI inside container
|
|
1627
|
+
cmd_update() {
|
|
1628
|
+
local container_name=$(get_container_name "$HOST_PATH")
|
|
1629
|
+
|
|
1630
|
+
# Check if container exists
|
|
1631
|
+
if ! docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then
|
|
1632
|
+
error "No container found for this directory"
|
|
1633
|
+
info "Run 'dclaude' first to create a persistent container"
|
|
1634
|
+
exit 1
|
|
1635
|
+
fi
|
|
1636
|
+
|
|
1637
|
+
local container_status=$(docker inspect --format='{{.State.Status}}' "$container_name" 2>/dev/null)
|
|
1638
|
+
|
|
1639
|
+
if [[ "$container_status" != "running" ]]; then
|
|
1640
|
+
error "Container is not running"
|
|
1641
|
+
info "Start it with 'dclaude' first"
|
|
1642
|
+
exit 1
|
|
1643
|
+
fi
|
|
1644
|
+
|
|
1645
|
+
info "Updating Claude CLI..."
|
|
1646
|
+
local npm_output
|
|
1647
|
+
if npm_output=$(docker exec -u claude "$container_name" npm update -g @anthropic-ai/claude-code 2>&1); then
|
|
1648
|
+
# Show command and output together, indented to align with "Debug: " prefix
|
|
1649
|
+
debug "npm update -g @anthropic-ai/claude-code
|
|
1650
|
+
$(echo "$npm_output" | sed 's/^/ /')"
|
|
1651
|
+
local new_version=$(docker exec -u claude "$container_name" claude --version 2>/dev/null | head -1)
|
|
1652
|
+
success "Claude CLI updated: $new_version"
|
|
1653
|
+
else
|
|
1654
|
+
debug "npm update -g @anthropic-ai/claude-code
|
|
1655
|
+
$(echo "$npm_output" | sed 's/^/ /')"
|
|
1656
|
+
error "Failed to update Claude CLI"
|
|
1657
|
+
exit 1
|
|
1658
|
+
fi
|
|
1659
|
+
exit 0
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
# Subcommand: stop container for current directory
|
|
1663
|
+
cmd_stop() {
|
|
1664
|
+
local container_name=$(get_container_name "$HOST_PATH")
|
|
1665
|
+
|
|
1666
|
+
# Check if container exists
|
|
1667
|
+
if ! docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then
|
|
1668
|
+
info "No container found for this directory"
|
|
1669
|
+
debug "Container name would be: $container_name"
|
|
1670
|
+
exit 0
|
|
1671
|
+
fi
|
|
1672
|
+
|
|
1673
|
+
local container_status=$(docker inspect --format='{{.State.Status}}' "$container_name" 2>/dev/null)
|
|
1674
|
+
|
|
1675
|
+
if [[ "$container_status" == "running" ]]; then
|
|
1676
|
+
# Count active tmux sessions
|
|
1677
|
+
local session_count
|
|
1678
|
+
session_count=$(docker exec -u claude "$container_name" tmux list-sessions 2>/dev/null | wc -l | tr -d '[:space:]') || session_count=0
|
|
1679
|
+
|
|
1680
|
+
if [[ "$session_count" =~ ^[0-9]+$ ]] && [[ "$session_count" -gt 0 ]]; then
|
|
1681
|
+
info "Stopping container $container_name ($session_count active session(s))..."
|
|
1682
|
+
else
|
|
1683
|
+
info "Stopping container $container_name..."
|
|
1684
|
+
fi
|
|
1685
|
+
|
|
1686
|
+
if docker stop "$container_name" >/dev/null; then
|
|
1687
|
+
success "Container stopped"
|
|
1688
|
+
else
|
|
1689
|
+
error "Failed to stop container"
|
|
1690
|
+
exit 1
|
|
1691
|
+
fi
|
|
1692
|
+
else
|
|
1693
|
+
info "Container $container_name is not running (status: $container_status)"
|
|
1694
|
+
fi
|
|
1695
|
+
exit 0
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
# Subcommand: SSH server for remote access (JetBrains Gateway, debugging, etc.)
|
|
1699
|
+
cmd_ssh() {
|
|
1700
|
+
local action=""
|
|
1701
|
+
|
|
1702
|
+
# Parse flags
|
|
1703
|
+
while [[ $# -gt 0 ]]; do
|
|
1704
|
+
case "$1" in
|
|
1705
|
+
--stop)
|
|
1706
|
+
action="stop"
|
|
1707
|
+
shift
|
|
1708
|
+
;;
|
|
1709
|
+
--help|-h)
|
|
1710
|
+
cat << 'SSH_HELP'
|
|
1711
|
+
dclaude ssh - SSH Server for Remote Access
|
|
1712
|
+
|
|
1713
|
+
Usage:
|
|
1714
|
+
dclaude ssh Start SSH server and show connection info
|
|
1715
|
+
dclaude ssh --stop Stop SSH server
|
|
1716
|
+
|
|
1717
|
+
Connection:
|
|
1718
|
+
ssh claude@localhost -p <port>
|
|
1719
|
+
Username: claude
|
|
1720
|
+
Password: claude
|
|
1721
|
+
|
|
1722
|
+
Use Cases:
|
|
1723
|
+
- JetBrains Gateway (PhpStorm, IntelliJ, WebStorm, etc.)
|
|
1724
|
+
- Remote debugging
|
|
1725
|
+
- SFTP file transfer
|
|
1726
|
+
- VS Code Remote SSH
|
|
1727
|
+
|
|
1728
|
+
JetBrains Gateway Setup:
|
|
1729
|
+
1. Start container: dclaude
|
|
1730
|
+
2. Start SSH: dclaude ssh
|
|
1731
|
+
3. Open JetBrains Gateway
|
|
1732
|
+
4. New Connection → SSH → localhost:<port shown above>
|
|
1733
|
+
5. Username: claude, Password: claude
|
|
1734
|
+
6. Gateway will download and install IDE backend automatically
|
|
1735
|
+
7. Select your project directory
|
|
1736
|
+
|
|
1737
|
+
SSH_HELP
|
|
1738
|
+
exit 0
|
|
1739
|
+
;;
|
|
1740
|
+
*)
|
|
1741
|
+
error "Unknown option: $1"
|
|
1742
|
+
info "Usage: dclaude ssh [--stop]"
|
|
1743
|
+
exit 1
|
|
1744
|
+
;;
|
|
1745
|
+
esac
|
|
1746
|
+
done
|
|
1747
|
+
|
|
1748
|
+
local container_name=$(get_container_name "$HOST_PATH")
|
|
1749
|
+
|
|
1750
|
+
# Check if container exists
|
|
1751
|
+
if ! docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then
|
|
1752
|
+
error "No container found for this directory"
|
|
1753
|
+
info "Run 'dclaude' first to create a persistent container"
|
|
1754
|
+
exit 1
|
|
1755
|
+
fi
|
|
1756
|
+
|
|
1757
|
+
local container_status=$(docker inspect --format='{{.State.Status}}' "$container_name" 2>/dev/null)
|
|
1758
|
+
|
|
1759
|
+
if [[ "$container_status" != "running" ]]; then
|
|
1760
|
+
if [[ "$container_status" == "exited" ]]; then
|
|
1761
|
+
info "Starting container: $container_name"
|
|
1762
|
+
docker start "$container_name" >/dev/null
|
|
1763
|
+
sleep 1
|
|
1764
|
+
else
|
|
1765
|
+
error "Container $container_name is in unexpected state: $container_status"
|
|
1766
|
+
exit 1
|
|
1767
|
+
fi
|
|
1768
|
+
fi
|
|
1769
|
+
|
|
1770
|
+
# Get SSH port from container label
|
|
1771
|
+
local ssh_port
|
|
1772
|
+
ssh_port=$(docker inspect --format='{{index .Config.Labels "dclaude.ssh.port"}}' "$container_name" 2>/dev/null)
|
|
1773
|
+
|
|
1774
|
+
if [[ -z "$ssh_port" ]]; then
|
|
1775
|
+
error "SSH port not configured for this container"
|
|
1776
|
+
echo ""
|
|
1777
|
+
info "This container was created with an older version of dclaude."
|
|
1778
|
+
info "To enable SSH, recreate the container:"
|
|
1779
|
+
echo ""
|
|
1780
|
+
echo " dclaude rm -f"
|
|
1781
|
+
echo " dclaude"
|
|
1782
|
+
echo " dclaude ssh"
|
|
1783
|
+
echo ""
|
|
1784
|
+
exit 1
|
|
1785
|
+
fi
|
|
1786
|
+
|
|
1787
|
+
case "$action" in
|
|
1788
|
+
stop)
|
|
1789
|
+
if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then
|
|
1790
|
+
info "Stopping SSH server..."
|
|
1791
|
+
docker exec -u root "$container_name" pkill -x sshd 2>/dev/null || true
|
|
1792
|
+
success "SSH server stopped"
|
|
1793
|
+
else
|
|
1794
|
+
info "SSH server is not running"
|
|
1795
|
+
fi
|
|
1796
|
+
;;
|
|
1797
|
+
|
|
1798
|
+
"")
|
|
1799
|
+
# Default: start SSH and show connection info
|
|
1800
|
+
# Start SSH if not running
|
|
1801
|
+
if docker exec "$container_name" pgrep -x sshd >/dev/null 2>&1; then
|
|
1802
|
+
info "SSH server already running"
|
|
1803
|
+
else
|
|
1804
|
+
info "Starting SSH server on port $ssh_port..."
|
|
1805
|
+
docker exec -u root "$container_name" sh -c "
|
|
1806
|
+
if [ ! -f /etc/ssh/ssh_host_rsa_key ]; then
|
|
1807
|
+
ssh-keygen -A >/dev/null 2>&1
|
|
1808
|
+
fi
|
|
1809
|
+
/usr/sbin/sshd -p $ssh_port
|
|
1810
|
+
"
|
|
1811
|
+
success "SSH server started"
|
|
1812
|
+
fi
|
|
1813
|
+
|
|
1814
|
+
echo ""
|
|
1815
|
+
echo "SSH Connection"
|
|
1816
|
+
echo "=============="
|
|
1817
|
+
echo " Host: localhost"
|
|
1818
|
+
echo " Port: $ssh_port"
|
|
1819
|
+
echo " Username: claude"
|
|
1820
|
+
echo " Password: claude"
|
|
1821
|
+
echo ""
|
|
1822
|
+
echo " ssh claude@localhost -p $ssh_port"
|
|
1823
|
+
echo ""
|
|
1824
|
+
;;
|
|
1825
|
+
esac
|
|
1826
|
+
|
|
1827
|
+
exit 0
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
# Subcommand: remove container for current directory
|
|
1831
|
+
cmd_rm() {
|
|
1832
|
+
local force=false
|
|
1833
|
+
|
|
1834
|
+
# Parse flags
|
|
1835
|
+
while [[ $# -gt 0 ]]; do
|
|
1836
|
+
case "$1" in
|
|
1837
|
+
-f|--force)
|
|
1838
|
+
force=true
|
|
1839
|
+
shift
|
|
1840
|
+
;;
|
|
1841
|
+
*)
|
|
1842
|
+
error "Unknown option: $1"
|
|
1843
|
+
info "Usage: dclaude rm [-f|--force]"
|
|
1844
|
+
exit 1
|
|
1845
|
+
;;
|
|
1846
|
+
esac
|
|
1847
|
+
done
|
|
1848
|
+
|
|
1849
|
+
local container_name=$(get_container_name "$HOST_PATH")
|
|
1850
|
+
|
|
1851
|
+
# Check if container exists
|
|
1852
|
+
if ! docker ps -a --format '{{.Names}}' | grep -q "^${container_name}$"; then
|
|
1853
|
+
info "No container found for this directory"
|
|
1854
|
+
debug "Container name would be: $container_name"
|
|
1855
|
+
exit 0
|
|
1856
|
+
fi
|
|
1857
|
+
|
|
1858
|
+
local container_status=$(docker inspect --format='{{.State.Status}}' "$container_name" 2>/dev/null)
|
|
1859
|
+
|
|
1860
|
+
if [[ "$container_status" == "running" ]]; then
|
|
1861
|
+
if [[ "$force" == "true" ]]; then
|
|
1862
|
+
# Count active tmux sessions
|
|
1863
|
+
local session_count
|
|
1864
|
+
session_count=$(docker exec -u claude "$container_name" tmux list-sessions 2>/dev/null | wc -l | tr -d '[:space:]') || session_count=0
|
|
1865
|
+
|
|
1866
|
+
if [[ "$session_count" =~ ^[0-9]+$ ]] && [[ "$session_count" -gt 0 ]]; then
|
|
1867
|
+
info "Removing running container $container_name ($session_count active session(s))..."
|
|
1868
|
+
else
|
|
1869
|
+
info "Removing running container $container_name..."
|
|
1870
|
+
fi
|
|
1871
|
+
|
|
1872
|
+
if docker rm -f "$container_name" >/dev/null; then
|
|
1873
|
+
success "Container removed"
|
|
1874
|
+
else
|
|
1875
|
+
error "Failed to remove container"
|
|
1876
|
+
exit 1
|
|
1877
|
+
fi
|
|
1878
|
+
else
|
|
1879
|
+
error "Container $container_name is running"
|
|
1880
|
+
info "Stop it first with: dclaude stop"
|
|
1881
|
+
info "Or force remove with: dclaude rm -f"
|
|
1882
|
+
exit 1
|
|
1883
|
+
fi
|
|
1884
|
+
else
|
|
1885
|
+
info "Removing container $container_name..."
|
|
1886
|
+
if docker rm "$container_name" >/dev/null; then
|
|
1887
|
+
success "Container removed"
|
|
1888
|
+
else
|
|
1889
|
+
error "Failed to remove container"
|
|
1890
|
+
exit 1
|
|
1891
|
+
fi
|
|
1892
|
+
fi
|
|
1893
|
+
exit 0
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
# Initialize HOST_PATH once for all commands
|
|
1897
|
+
HOST_PATH=$(get_host_path)
|
|
1898
|
+
|
|
1899
|
+
# Handle subcommands
|
|
1900
|
+
if [[ $# -gt 0 ]]; then
|
|
1901
|
+
case "$1" in
|
|
1902
|
+
new)
|
|
1903
|
+
# Explicit "new" command - shift and pass remaining args to main
|
|
1904
|
+
shift
|
|
1905
|
+
# Fall through to main function
|
|
1906
|
+
;;
|
|
1907
|
+
attach)
|
|
1908
|
+
shift
|
|
1909
|
+
cmd_attach "$@"
|
|
1910
|
+
;;
|
|
1911
|
+
exec|shell|bash)
|
|
1912
|
+
shift
|
|
1913
|
+
cmd_exec "$@"
|
|
1914
|
+
;;
|
|
1915
|
+
chrome)
|
|
1916
|
+
shift
|
|
1917
|
+
cmd_chrome "$@"
|
|
1918
|
+
;;
|
|
1919
|
+
gh)
|
|
1920
|
+
shift
|
|
1921
|
+
cmd_gh "$@"
|
|
1922
|
+
;;
|
|
1923
|
+
pull)
|
|
1924
|
+
shift
|
|
1925
|
+
cmd_pull "$@"
|
|
1926
|
+
;;
|
|
1927
|
+
update)
|
|
1928
|
+
shift
|
|
1929
|
+
cmd_update "$@"
|
|
1930
|
+
;;
|
|
1931
|
+
stop)
|
|
1932
|
+
shift
|
|
1933
|
+
cmd_stop "$@"
|
|
1934
|
+
;;
|
|
1935
|
+
rm)
|
|
1936
|
+
shift
|
|
1937
|
+
cmd_rm "$@"
|
|
1938
|
+
;;
|
|
1939
|
+
ssh)
|
|
1940
|
+
shift
|
|
1941
|
+
cmd_ssh "$@"
|
|
1942
|
+
;;
|
|
1943
|
+
--help|-h|help)
|
|
1944
|
+
cat << 'EOF'
|
|
1945
|
+
dclaude - Dockerized Claude Code Launcher
|
|
1946
|
+
|
|
1947
|
+
Usage:
|
|
1948
|
+
dclaude [options] Start new Claude session (default)
|
|
1949
|
+
dclaude new [options] Start new Claude session (explicit)
|
|
1950
|
+
dclaude attach <session> Attach to existing tmux session
|
|
1951
|
+
dclaude pull Pull latest Docker image
|
|
1952
|
+
dclaude update Update Claude CLI inside container
|
|
1953
|
+
dclaude stop Stop container for current directory
|
|
1954
|
+
dclaude rm [-f] Remove container for current directory
|
|
1955
|
+
dclaude ssh Start SSH server for remote access
|
|
1956
|
+
dclaude chrome [options] Launch Chrome with DevTools and MCP integration
|
|
1957
|
+
dclaude gh Authenticate GitHub CLI (runs gh auth login)
|
|
1958
|
+
dclaude exec [command] Execute command in container (default: bash)
|
|
1959
|
+
dclaude shell Open bash shell in container
|
|
1960
|
+
dclaude --help Show this help
|
|
1961
|
+
|
|
1962
|
+
Environment Variables:
|
|
1963
|
+
DCLAUDE_TAG Docker image tag (default: latest)
|
|
1964
|
+
DCLAUDE_RM Remove container on exit (default: false)
|
|
1965
|
+
DCLAUDE_DEBUG Enable debug output (default: false)
|
|
1966
|
+
DCLAUDE_MOUNT_CONFIGS Mount host configs (default: false)
|
|
1967
|
+
DCLAUDE_GIT_AUTH SSH auth for Git: auto, agent-forwarding, key-mount, none
|
|
1968
|
+
DCLAUDE_NETWORK Network mode: auto, host, bridge
|
|
1969
|
+
DCLAUDE_DOCKER_SOCKET Override Docker socket path
|
|
1970
|
+
DCLAUDE_TMUX_SESSION Custom tmux session name (default: claude-TIMESTAMP)
|
|
1971
|
+
DCLAUDE_CHROME_BIN Chrome executable path (auto-detected if not set)
|
|
1972
|
+
DCLAUDE_CHROME_PROFILE Chrome profile name (default: claude)
|
|
1973
|
+
DCLAUDE_CHROME_PORT Chrome debugging port (default: 9222)
|
|
1974
|
+
DCLAUDE_CHROME_FLAGS Additional Chrome flags (default: empty)
|
|
1975
|
+
|
|
1976
|
+
Examples:
|
|
1977
|
+
# Start new Claude session (auto-generated session name)
|
|
1978
|
+
dclaude
|
|
1979
|
+
dclaude new
|
|
1980
|
+
|
|
1981
|
+
# Start with custom session name (role-based workflows)
|
|
1982
|
+
DCLAUDE_TMUX_SESSION=claude-architect dclaude
|
|
1983
|
+
dclaude new --dangerously-skip-permissions
|
|
1984
|
+
|
|
1985
|
+
# Attach to existing named session
|
|
1986
|
+
dclaude attach claude-architect
|
|
1987
|
+
DCLAUDE_TMUX_SESSION=claude-architect dclaude attach
|
|
1988
|
+
|
|
1989
|
+
# Start with ephemeral container (removed on exit)
|
|
1990
|
+
DCLAUDE_RM=true dclaude
|
|
1991
|
+
|
|
1992
|
+
# Update image and restart container
|
|
1993
|
+
dclaude pull # Pull latest image
|
|
1994
|
+
dclaude update # Update Claude CLI in container
|
|
1995
|
+
dclaude stop # Stop container (preserves it)
|
|
1996
|
+
dclaude rm # Remove stopped container
|
|
1997
|
+
dclaude rm -f # Force remove running container
|
|
1998
|
+
|
|
1999
|
+
# SSH server for remote access (JetBrains Gateway, VS Code Remote, etc.)
|
|
2000
|
+
dclaude ssh # Start SSH server, show connection info
|
|
2001
|
+
dclaude ssh --stop # Stop SSH server
|
|
2002
|
+
|
|
2003
|
+
# Launch Chrome with DevTools for MCP integration
|
|
2004
|
+
dclaude chrome
|
|
2005
|
+
dclaude chrome --port=9223 # Custom debugging port
|
|
2006
|
+
dclaude chrome --setup-only # Just create .mcp.json, don't launch
|
|
2007
|
+
DCLAUDE_CHROME_PROFILE=testing dclaude chrome # Use different profile
|
|
2008
|
+
|
|
2009
|
+
# Authenticate GitHub CLI (persists in dclaude-config volume)
|
|
2010
|
+
dclaude gh
|
|
2011
|
+
|
|
2012
|
+
# Open bash shell in running container
|
|
2013
|
+
dclaude exec
|
|
2014
|
+
|
|
2015
|
+
# Run command in container
|
|
2016
|
+
dclaude exec npm install
|
|
2017
|
+
|
|
2018
|
+
For more information: https://github.com/alanbem/dclaude
|
|
2019
|
+
EOF
|
|
2020
|
+
exit 0
|
|
2021
|
+
;;
|
|
2022
|
+
esac
|
|
2023
|
+
fi
|
|
2024
|
+
|
|
2025
|
+
# Handle signals
|
|
2026
|
+
|
|
2027
|
+
trap 'exit 130' INT
|
|
2028
|
+
trap 'exit 143' TERM
|
|
2029
|
+
|
|
2030
|
+
# Run main function
|
|
2031
|
+
main "$@"
|