-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun.sh
More file actions
executable file
·371 lines (323 loc) · 11.6 KB
/
run.sh
File metadata and controls
executable file
·371 lines (323 loc) · 11.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
#!/bin/bash
set -euo pipefail
# Entry point for the containerized dev agent.
#
# Usage: ./run.sh [options] [project-dir]
# project-dir Path to mount as workspace (default: current directory)
# --opencode Use OpenCode agent instead of Claude Code
# --build Force rebuild images
# --shell Start bash instead of the agent
# --down Stop and remove containers
# --reload-proxy Hot-reload squid config (squid -k reconfigure)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BASE_COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml"
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
FORCE_BUILD=false
START_SHELL=false
ACTION=run
# Agent selection (default: Claude Code)
AGENT_SERVICE=agent
AGENT_PROFILE=claude
AGENT_LABEL="Claude Code"
usage() {
echo "Usage: $0 [options] [project-dir]"
echo ""
echo " project-dir Path to mount as workspace (default: current directory)"
echo " --opencode Use OpenCode agent instead of Claude Code"
echo " --build Force rebuild images"
echo " --shell Start bash instead of the agent"
echo " --down Stop and remove containers"
echo " --reload-proxy Hot-reload squid config"
echo " -h, --help Show this help"
}
# Parse arguments
PROJECT_DIR=""
while [[ $# -gt 0 ]]; do
case $1 in
--opencode)
AGENT_SERVICE=opencode
AGENT_PROFILE=opencode
AGENT_LABEL="OpenCode"
shift
;;
--build)
FORCE_BUILD=true
shift
;;
--shell)
START_SHELL=true
shift
;;
--down)
ACTION=down
shift
;;
--reload-proxy)
ACTION=reload
shift
;;
--help|-h)
usage
exit 0
;;
-*)
echo -e "${RED}Unknown option: $1${NC}"
usage
exit 1
;;
*)
PROJECT_DIR="$1"
shift
;;
esac
done
# Resolve project directory
if [ -z "$PROJECT_DIR" ]; then
PROJECT_DIR="$(pwd)"
fi
PROJECT_DIR="$(cd "$PROJECT_DIR" && pwd)"
if [ ! -d "$PROJECT_DIR" ]; then
echo -e "${RED}Error: Project directory does not exist: $PROJECT_DIR${NC}"
exit 1
fi
export PROJECT_DIR
# --- Auth mode detection ---
# Detect Bedrock vs Max by inspecting the host's global Claude settings.
# If CLAUDE_CODE_USE_BEDROCK is set in ~/.claude/settings.json, use Bedrock;
# otherwise assume a Claude Max subscription.
detect_auth_mode() {
local settings="${HOME}/.claude/settings.json"
if [ -f "$settings" ] && command -v jq &>/dev/null; then
if jq -e '.env.CLAUDE_CODE_USE_BEDROCK' "$settings" &>/dev/null; then
return 0
fi
fi
return 1
}
if detect_auth_mode; then
DEV_AGENT_AUTH=bedrock
export CLAUDE_CODE_USE_BEDROCK=1
export CLAUDE_SETTINGS_FILE="$SCRIPT_DIR/dot-claude/settings.json"
else
DEV_AGENT_AUTH=max
unset CLAUDE_CODE_USE_BEDROCK 2>/dev/null || true
export CLAUDE_SETTINGS_FILE="$SCRIPT_DIR/dot-claude/settings.max.json"
fi
# Ensure Claude state file exists (Docker would create a directory otherwise)
if [ "$AGENT_SERVICE" = "agent" ]; then
CLAUDE_STATE_FILE="${HOME}/.claude.json"
if [ ! -f "$CLAUDE_STATE_FILE" ]; then
echo '{}' > "$CLAUDE_STATE_FILE"
fi
fi
# Derive project name from workspace directory basename
PROJECT_BASENAME="$(basename "$PROJECT_DIR")"
PROJECT_NAME="dev-agent-${PROJECT_BASENAME}"
# Temp directory for generated files (unique per project)
TEMP_DIR="/tmp/${PROJECT_NAME}"
# Build the compose command arguments
COMPOSE_ARGS=(-p "$PROJECT_NAME" -f "$BASE_COMPOSE_FILE" --profile "$AGENT_PROFILE")
# --- Workspace config parsing ---
WORKSPACE_CONFIG="$PROJECT_DIR/.dev-agent/config.yml"
WORKSPACE_COMPOSE_OVERRIDE="$PROJECT_DIR/.dev-agent/docker-compose.override.yml"
parse_workspace_config() {
if [ ! -f "$WORKSPACE_CONFIG" ]; then
return
fi
# Verify yq is available
if ! command -v yq &>/dev/null; then
echo -e "${RED}Error: yq is required to parse .dev-agent/config.yml${NC}"
echo "Install: brew install yq (macOS) or see https://github.com/mikefarah/yq"
exit 1
fi
echo -e "${GREEN}Found workspace config: .dev-agent/config.yml${NC}"
mkdir -p "$TEMP_DIR"
# --- Merge allowed domains ---
local extra_domains
extra_domains=$(yq -r '.allowed_domains // [] | .[]' "$WORKSPACE_CONFIG" 2>/dev/null)
if [ -n "$extra_domains" ]; then
echo " Merging allowed domains..."
cat "$SCRIPT_DIR/allowed-domains.txt" > "$TEMP_DIR/allowed-domains.txt"
echo "" >> "$TEMP_DIR/allowed-domains.txt"
echo "# Workspace-specific domains" >> "$TEMP_DIR/allowed-domains.txt"
echo "$extra_domains" >> "$TEMP_DIR/allowed-domains.txt"
fi
# --- Generate compose override ---
local override="services:"
local has_agent_overrides=false
# Environment variables
local env_keys
env_keys=$(yq -r '.env // {} | keys | .[]' "$WORKSPACE_CONFIG" 2>/dev/null)
if [ -n "$env_keys" ]; then
has_agent_overrides=true
override+=$'\n'" ${AGENT_SERVICE}:"$'\n'" environment:"
while IFS= read -r key; do
local value
value=$(yq -r ".env.\"$key\"" "$WORKSPACE_CONFIG")
override+=$'\n'" - ${key}=${value}"
done <<< "$env_keys"
fi
# Extra volumes
local volumes
volumes=$(yq -r '.volumes // [] | .[]' "$WORKSPACE_CONFIG" 2>/dev/null)
if [ -n "$volumes" ]; then
if [ "$has_agent_overrides" = false ]; then
override+=$'\n'" ${AGENT_SERVICE}:"
has_agent_overrides=true
fi
override+=$'\n'" volumes:"
while IFS= read -r vol; do
local host_path container_rest
host_path=$(echo "$vol" | cut -d: -f1)
container_rest=$(echo "$vol" | cut -d: -f2-)
if [[ "$host_path" != /* ]]; then
host_path="$PROJECT_DIR/$host_path"
fi
override+=$'\n'" - ${host_path}:${container_rest}"
done <<< "$volumes"
fi
# External networks
local networks
networks=$(yq -r '.networks // [] | .[]' "$WORKSPACE_CONFIG" 2>/dev/null)
if [ -n "$networks" ]; then
if [ "$has_agent_overrides" = false ]; then
override+=$'\n'" ${AGENT_SERVICE}:"
has_agent_overrides=true
fi
override+=$'\n'" networks:"
while IFS= read -r net; do
override+=$'\n'" - ${net}"
done <<< "$networks"
# Declare the external networks at top level
override+=$'\n'$'\n'"networks:"
while IFS= read -r net; do
override+=$'\n'" ${net}:"
override+=$'\n'" external: true"
done <<< "$networks"
fi
# Write the generated override if we have anything
if [ "$has_agent_overrides" = true ]; then
echo "$override" > "$TEMP_DIR/generated-override.yml"
echo " Generated compose override"
fi
# Override proxy domain list if we merged domains
if [ -f "$TEMP_DIR/allowed-domains.txt" ]; then
local proxy_override="services:"
proxy_override+=$'\n'" proxy:"
proxy_override+=$'\n'" volumes:"
proxy_override+=$'\n'" - ${TEMP_DIR}/allowed-domains.txt:/etc/squid/allowed-domains.txt:ro"
proxy_override+=$'\n'" - ${SCRIPT_DIR}/squid.conf:/etc/squid/squid.conf:ro"
echo "$proxy_override" > "$TEMP_DIR/proxy-override.yml"
echo " Merged domain allowlist"
fi
}
parse_workspace_config
# --- Git worktree detection ---
detect_worktree() {
if [ ! -f "$PROJECT_DIR/.git" ]; then
return
fi
# .git is a file, not a directory — this is a git worktree.
# Read the gitdir reference (e.g. "gitdir: /path/to/repo/.git/worktrees/branch")
local gitdir_ref
gitdir_ref=$(sed 's/^gitdir: //' "$PROJECT_DIR/.git")
# Resolve to absolute path (may be relative)
if [[ "$gitdir_ref" != /* ]]; then
gitdir_ref=$(cd "$PROJECT_DIR" && cd "$(dirname "$gitdir_ref")" && pwd)/$(basename "$gitdir_ref")
fi
if [ ! -d "$gitdir_ref" ]; then
echo -e "${YELLOW}Warning: worktree gitdir not found: $gitdir_ref${NC}"
return
fi
# Find the main .git directory via the commondir file
local main_git_dir
if [ -f "$gitdir_ref/commondir" ]; then
local commondir
commondir=$(cat "$gitdir_ref/commondir")
main_git_dir=$(cd "$gitdir_ref" && cd "$commondir" && pwd)
else
# Fallback: standard layout .git/worktrees/<name> → .git is two levels up
main_git_dir=$(cd "$gitdir_ref/../.." && pwd)
fi
echo -e "${GREEN}Detected git worktree${NC}"
echo " Main .git dir: $main_git_dir"
# Mount the main .git directory at the same absolute host path so the
# gitdir reference in the worktree's .git file resolves correctly.
mkdir -p "$TEMP_DIR"
cat > "$TEMP_DIR/worktree-override.yml" <<YAML
services:
${AGENT_SERVICE}:
volumes:
- ${main_git_dir}:${main_git_dir}
YAML
}
detect_worktree
# Add generated overrides to compose args (if they exist)
if [ -f "$TEMP_DIR/proxy-override.yml" ]; then
COMPOSE_ARGS+=(-f "$TEMP_DIR/proxy-override.yml")
fi
if [ -f "$TEMP_DIR/generated-override.yml" ]; then
COMPOSE_ARGS+=(-f "$TEMP_DIR/generated-override.yml")
fi
if [ -f "$TEMP_DIR/worktree-override.yml" ]; then
COMPOSE_ARGS+=(-f "$TEMP_DIR/worktree-override.yml")
fi
# Add workspace compose override last (so it wins on conflicts)
if [ -f "$WORKSPACE_COMPOSE_OVERRIDE" ]; then
echo -e "${GREEN}Found workspace compose override: .dev-agent/docker-compose.override.yml${NC}"
COMPOSE_ARGS+=(-f "$WORKSPACE_COMPOSE_OVERRIDE")
fi
# --- Actions ---
case $ACTION in
down)
echo -e "${YELLOW}Stopping containers for ${PROJECT_NAME}...${NC}"
docker compose "${COMPOSE_ARGS[@]}" down --remove-orphans
rm -rf "$TEMP_DIR"
echo -e "${GREEN}Done.${NC}"
exit 0
;;
reload)
echo -e "${YELLOW}Reloading squid configuration for ${PROJECT_NAME}...${NC}"
docker compose "${COMPOSE_ARGS[@]}" exec proxy squid -k reconfigure
echo -e "${GREEN}Squid configuration reloaded.${NC}"
exit 0
;;
esac
# Build if requested
if [ "$FORCE_BUILD" = true ]; then
echo -e "${GREEN}Building images...${NC}"
docker compose "${COMPOSE_ARGS[@]}" build
fi
echo -e "${GREEN}Starting agent environment (${AGENT_LABEL})...${NC}"
echo "Project: $PROJECT_DIR"
echo "Instance: $PROJECT_NAME"
echo "Auth: $DEV_AGENT_AUTH"
echo ""
# Start all background services (everything except the active agent, which runs interactively)
BACKGROUND_SERVICES=$(docker compose "${COMPOSE_ARGS[@]}" config --services | grep -v "^${AGENT_SERVICE}$")
if [ -n "$BACKGROUND_SERVICES" ]; then
docker compose "${COMPOSE_ARGS[@]}" up -d $BACKGROUND_SERVICES
fi
# Clean up on exit
cleanup() {
local exit_code=$?
echo ""
echo -e "${YELLOW}Cleaning up ${PROJECT_NAME}...${NC}"
docker compose "${COMPOSE_ARGS[@]}" down --remove-orphans
rm -rf "$TEMP_DIR"
exit $exit_code
}
trap cleanup EXIT INT TERM
# Run agent interactively (auto-removed on exit)
if [ "$START_SHELL" = true ]; then
echo -e "${YELLOW}Starting shell (type '$([ "$AGENT_SERVICE" = "agent" ] && echo "claude" || echo "opencode")' to launch ${AGENT_LABEL})${NC}"
echo ""
docker compose "${COMPOSE_ARGS[@]}" run --rm "$AGENT_SERVICE" bash
else
echo -e "${YELLOW}Starting ${AGENT_LABEL}...${NC}"
echo ""
docker compose "${COMPOSE_ARGS[@]}" run --rm "$AGENT_SERVICE"
fi