sanduary 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,10 +20,11 @@ RUN apt-get update && apt-get install -y \
20
20
  # 開発に便利なツール
21
21
  RUN apt-get update && apt-get install -y \
22
22
  git \
23
- vim \
24
23
  jq \
25
- zip \
26
24
  less \
25
+ lsof \
26
+ vim \
27
+ zip \
27
28
  && apt-get clean \
28
29
  && rm -rf /var/lib/apt/lists/*
29
30
 
@@ -5,7 +5,7 @@
5
5
  "workspaceMount": "",
6
6
  "updateRemoteUserUID": true,
7
7
  "initializeCommand": "echo 'PROJECT_ROOT='$(pwd) > .devcontainer/.env && echo 'PROJECT_NAME='$(basename $(pwd)) >> .devcontainer/.env && echo 'GIT_ORIGIN_URL='$(git remote get-url origin 2>/dev/null || echo '') >> .devcontainer/.env && [ -f ~/.claude-sandbox-credentials.json ] || echo '{}' > ~/.claude-sandbox-credentials.json && [ -f ~/.claude-sandbox.json ] || echo '{}' > ~/.claude-sandbox.json && [ -d ~/.claude/plugins ] || mkdir -p ~/.claude/plugins",
8
- "postCreateCommand": "git init && git remote add origin \"${GIT_ORIGIN_URL:-/host-project}\" && git fetch && git checkout $(git -C /host-project branch --show-current) && echo 'alias claude=\"npx claude --dangerously-skip-permissions\"' >> ~/.bashrc",
8
+ "postCreateCommand": "git init && git remote add origin \"${GIT_ORIGIN_URL}\" && git fetch && git checkout $(git -C /host-project branch --show-current) && echo 'alias claude=\"npx claude --dangerously-skip-permissions\"' >> ~/.bashrc",
9
9
  "postStartCommand": "cp -rT /home/node/.claude-host-plugins /home/node/.claude/plugins 2>/dev/null || true; [ -f /home/node/.claude/plugins/known_marketplaces.json ] && sed -i 's|/Users/[^/]*/|/home/node/|g' /home/node/.claude/plugins/known_marketplaces.json; [ -f /home/node/.claude/plugins/installed_plugins.json ] && sed -i 's|/Users/[^/]*/|/home/node/|g' /home/node/.claude/plugins/installed_plugins.json; true",
10
10
  "shutdownAction": "stopCompose",
11
11
  "remoteUser": "node",
@@ -1,6 +1,5 @@
1
1
  services:
2
2
  devcontainer:
3
- platform: linux/amd64
4
3
  build:
5
4
  context: .
6
5
  dockerfile: Dockerfile
@@ -14,7 +13,8 @@ services:
14
13
  - ${HOME}/.claude:/home/node/.claude
15
14
  - ${HOME}/.claude/plugins:/home/node/.claude-host-plugins:ro
16
15
  - /home/node/.claude/plugins
17
- - ${HOME}/.claude-sandbox.json:/home/node/.claude.json
16
+ # .claude.jsonは共有しない(並列実行時の書き込み競合を回避)
17
+ # 各コンテナで独立して自動生成される
18
18
  - ${HOME}/.claude-sandbox-credentials.json:/home/node/.claude/.credentials.json
19
19
  - ${HOME}/.gitconfig:/home/node/.gitconfig
20
20
  working_dir: /workspaces/${PROJECT_NAME:-project}
@@ -25,4 +25,4 @@ services:
25
25
  GIT_CONFIG_COUNT: 1
26
26
  GIT_CONFIG_KEY_0: safe.directory
27
27
  GIT_CONFIG_VALUE_0: /workspaces/${PROJECT_NAME:-project}
28
- GIT_ORIGIN_URL: ${GIT_ORIGIN_URL:-/host-project}
28
+ GIT_ORIGIN_URL: ${GIT_ORIGIN_URL}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanduary",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "Development sandbox environment for AI agents - A secure sanctuary for running AI coding assistants in Docker DevContainers",
5
5
  "keywords": [
6
6
  "devcontainer",
@@ -6,83 +6,700 @@ SCRIPT_PATH="$(realpath "$0")"
6
6
  SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
7
7
  PACKAGE_ROOT="$(dirname "$SCRIPT_DIR")"
8
8
 
9
- COMMAND="${1:-run}"
9
+ # ================================================
10
+ # カラーコード(ターミナル出力用)
11
+ # ================================================
12
+ RED='\033[0;31m'
13
+ GREEN='\033[0;32m'
14
+ YELLOW='\033[1;33m'
15
+ NC='\033[0m' # No Color
10
16
 
11
- case "$COMMAND" in
12
- init)
13
- node "$SCRIPT_DIR/postinstall.js"
14
- ;;
15
- run)
16
- # プロジェクトルートをgitから取得
17
- PROJECT_ROOT="$(git rev-parse --show-toplevel)"
18
- export PROJECT_ROOT
17
+ # ================================================
18
+ # ユーティリティ関数
19
+ # ================================================
20
+
21
+ # Dockerが起動しているか確認
22
+ check_docker() {
23
+ local json_output="${1:-false}"
24
+ if ! command -v docker &> /dev/null; then
25
+ if [[ "$json_output" == true ]]; then
26
+ echo '{"error": "Docker is not installed"}'
27
+ else
28
+ echo -e "${RED}Error: Docker is not installed${NC}" >&2
29
+ fi
30
+ exit 1
31
+ fi
32
+
33
+ if ! docker info &> /dev/null; then
34
+ if [[ "$json_output" == true ]]; then
35
+ echo '{"error": "Docker is not running"}'
36
+ else
37
+ echo -e "${RED}Error: Docker is not running${NC}" >&2
38
+ fi
39
+ exit 1
40
+ fi
41
+ }
42
+
43
+ # 現在のプロジェクト名を取得
44
+ get_current_project() {
45
+ if [[ -f "package.json" ]]; then
46
+ grep '"name"' package.json | head -1 | sed 's/.*"name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/'
47
+ else
48
+ echo ""
49
+ fi
50
+ }
51
+
52
+ # devcontainer一覧を取得
53
+ get_devcontainers() {
54
+ docker ps --format "{{.Names}}" | grep -E "devcontainer" || true
55
+ }
56
+
57
+ # コンテナ内のプロジェクト名を取得
58
+ get_container_project() {
59
+ local container_name=$1
60
+ local project_name=""
61
+
62
+ # コンテナ内のpackage.jsonからプロジェクト名を取得
63
+ # 一般的なdevcontainerのワークスペースパスを試行
64
+ for workspace_path in "/workspaces" "/workspace" "/app" "/home"; do
65
+ project_name=$(docker exec "$container_name" bash -c "
66
+ for dir in ${workspace_path}/*/; do
67
+ if [[ -f \"\${dir}package.json\" ]]; then
68
+ grep '\"name\"' \"\${dir}package.json\" 2>/dev/null | head -1 | sed 's/.*\"name\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/'
69
+ break
70
+ fi
71
+ done
72
+ " 2>/dev/null || true)
73
+
74
+ if [[ -n "$project_name" ]]; then
75
+ echo "$project_name"
76
+ return
77
+ fi
78
+ done
79
+
80
+ # ルートディレクトリのpackage.jsonも確認
81
+ project_name=$(docker exec "$container_name" bash -c "
82
+ if [[ -f /package.json ]]; then
83
+ grep '\"name\"' /package.json 2>/dev/null | head -1 | sed 's/.*\"name\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/'
84
+ fi
85
+ " 2>/dev/null || true)
86
+
87
+ echo "${project_name:-unknown}"
88
+ }
89
+
90
+ # サブモジュール一覧を取得
91
+ get_submodules() {
92
+ if [[ -f ".gitmodules" ]]; then
93
+ grep 'path = ' .gitmodules | sed 's/.*path = //'
94
+ fi
95
+ }
96
+
97
+ # コンテナが空きかどうか判定(Claude Codeワンライナー実行中かどうか)
98
+ is_container_available() {
99
+ local container_name=$1
100
+
101
+ # pgrep -af claude | grep -E " -p( |$)" でワンライナー実行中かどうか確認
102
+ # 結果があれば使用中(false)、なければ空き(true)
103
+ local result
104
+ result=$(docker exec "$container_name" bash -c 'pgrep -af claude 2>/dev/null | grep -E " -p( |$)"' 2>/dev/null || true)
105
+
106
+ if [[ -z "$result" ]]; then
107
+ echo "true"
108
+ else
109
+ echo "false"
110
+ fi
111
+ }
112
+
113
+ # ================================================
114
+ # containers サブコマンド用関数
115
+ # ================================================
116
+
117
+ # テーブル形式で出力
118
+ output_table() {
119
+ local current_project=$1
120
+ shift
121
+ local containers=("$@")
122
+ local matched_count=0
123
+ local available_count=0
124
+
125
+ echo ""
126
+ echo "## 検出されたコンテナ"
127
+ echo ""
128
+ echo "| コンテナ名 | プロジェクト | 状態 | 空き |"
129
+ echo "|-----------|-------------|------|-----|"
130
+
131
+ for container in "${containers[@]}"; do
132
+ local project
133
+ project=$(get_container_project "$container")
134
+
135
+ local matched_status
136
+ local available_status
137
+ local is_matched=false
138
+
139
+ # マッチ判定
140
+ if [[ "$project" == "$current_project" ]]; then
141
+ matched_status="✅ マッチ"
142
+ is_matched=true
143
+ ((matched_count++))
144
+ else
145
+ matched_status="❌ 別プロジェクト"
146
+ fi
147
+
148
+ # 空き判定(マッチしたコンテナのみ)
149
+ if [[ "$is_matched" == true ]]; then
150
+ if [[ $(is_container_available "$container") == "true" ]]; then
151
+ available_status="✅ 空き"
152
+ ((available_count++))
153
+ else
154
+ available_status="❌ 使用中"
155
+ fi
156
+ else
157
+ available_status="-"
158
+ fi
159
+
160
+ echo "| $container | $project | $matched_status | $available_status |"
161
+ done
162
+
163
+ echo ""
164
+ echo "マッチしたコンテナ: ${matched_count}個"
165
+ echo "空きコンテナ: ${available_count}個"
166
+ }
167
+
168
+ # JSON形式で出力
169
+ output_json() {
170
+ local current_project=$1
171
+ shift
172
+ local containers=("$@")
173
+ local matched_count=0
174
+ local available_count=0
175
+ local json_containers=""
176
+ local first=true
177
+
178
+ for container in "${containers[@]}"; do
179
+ local project
180
+ project=$(get_container_project "$container")
181
+
182
+ local matched=false
183
+ local available=false
184
+
185
+ # マッチ判定
186
+ if [[ "$project" == "$current_project" ]]; then
187
+ matched=true
188
+ ((matched_count++))
189
+
190
+ # 空き判定(マッチしたコンテナのみ)
191
+ if [[ $(is_container_available "$container") == "true" ]]; then
192
+ available=true
193
+ ((available_count++))
194
+ fi
195
+ fi
196
+
197
+ # JSON配列要素を構築
198
+ if [[ "$first" == true ]]; then
199
+ first=false
200
+ else
201
+ json_containers+=","
202
+ fi
203
+
204
+ json_containers+="
205
+ {
206
+ \"name\": \"$container\",
207
+ \"project\": \"$project\",
208
+ \"matched\": $matched,
209
+ \"available\": $available
210
+ }"
211
+ done
212
+
213
+ cat << EOF
214
+ {
215
+ "currentProject": "$current_project",
216
+ "containers": [$json_containers
217
+ ],
218
+ "matchedCount": $matched_count,
219
+ "availableCount": $available_count
220
+ }
221
+ EOF
222
+ }
223
+
224
+ # containers コマンドのメイン処理
225
+ cmd_containers() {
226
+ local json_output=false
227
+
228
+ # 引数解析
229
+ while [[ $# -gt 0 ]]; do
230
+ case $1 in
231
+ --json)
232
+ json_output=true
233
+ shift
234
+ ;;
235
+ *)
236
+ echo "Unknown option: $1" >&2
237
+ exit 1
238
+ ;;
239
+ esac
240
+ done
241
+
242
+ # Dockerの起動確認
243
+ check_docker "$json_output"
244
+
245
+ # 現在のプロジェクト名を取得
246
+ local current_project
247
+ current_project=$(get_current_project)
248
+
249
+ if [[ -z "$current_project" ]]; then
250
+ if [[ "$json_output" == true ]]; then
251
+ echo '{"error": "Could not determine current project (package.json not found)"}'
252
+ else
253
+ echo -e "${YELLOW}Warning: Could not determine current project (package.json not found)${NC}" >&2
254
+ fi
255
+ exit 1
256
+ fi
257
+
258
+ # devcontainer一覧を取得
259
+ local containers_raw
260
+ containers_raw=$(get_devcontainers)
261
+
262
+ if [[ -z "$containers_raw" ]]; then
263
+ if [[ "$json_output" == true ]]; then
264
+ echo "{\"currentProject\": \"$current_project\", \"containers\": [], \"matchedCount\": 0, \"availableCount\": 0}"
265
+ else
266
+ echo ""
267
+ echo "## 検出されたコンテナ"
268
+ echo ""
269
+ echo "devcontainerが見つかりませんでした。"
270
+ fi
271
+ exit 0
272
+ fi
273
+
274
+ # 配列に変換
275
+ local containers=()
276
+ while IFS= read -r line; do
277
+ containers+=("$line")
278
+ done <<< "$containers_raw"
279
+
280
+ # 出力
281
+ if [[ "$json_output" == true ]]; then
282
+ output_json "$current_project" "${containers[@]}"
283
+ else
284
+ output_table "$current_project" "${containers[@]}"
285
+ fi
286
+ }
287
+
288
+ # ================================================
289
+ # sync-submodules サブコマンド用関数
290
+ # ================================================
291
+
292
+ # サブモジュールをコンテナに同期
293
+ sync_submodules_to_container() {
294
+ local container_name=$1
295
+ local submodules
296
+ submodules=$(get_submodules)
297
+
298
+ if [[ -z "$submodules" ]]; then
299
+ echo "サブモジュールが見つかりません。"
300
+ return 0
301
+ fi
302
+
303
+ echo "## サブモジュール同期"
304
+ echo ""
305
+ echo "対象コンテナ: $container_name"
306
+ echo ""
307
+
308
+ # コンテナ内のワークスペースパスを検出
309
+ local workspace_path
310
+ workspace_path=$(docker exec "$container_name" bash -c '
311
+ for dir in /workspaces/*/; do
312
+ if [[ -f "${dir}package.json" ]]; then
313
+ echo "${dir%/}"
314
+ break
315
+ fi
316
+ done
317
+ ' 2>/dev/null)
318
+
319
+ if [[ -z "$workspace_path" ]]; then
320
+ echo -e "${RED}Error: コンテナ内のワークスペースが見つかりません${NC}" >&2
321
+ return 1
322
+ fi
323
+
324
+ echo "ワークスペースパス: $workspace_path"
325
+ echo ""
326
+
327
+ while IFS= read -r submodule_path; do
328
+ [[ -z "$submodule_path" ]] && continue
19
329
 
20
- DEVCONTAINER_DIR="$PROJECT_ROOT/.devcontainer"
21
- DEVCONTAINER_JSON="$PROJECT_ROOT/.devcontainer/devcontainer.json"
330
+ echo "### $submodule_path"
22
331
 
23
- # ランダムなプロジェクト名を生成(sandbox-XXXX形式)
24
- PROJECT_NAME="sandbox-$(head -c 4 /dev/urandom | xxd -p)"
25
- export PROJECT_NAME
332
+ # サブモジュールディレクトリのコピー
333
+ if [[ -d "$submodule_path" ]]; then
334
+ local parent_dir
335
+ parent_dir=$(dirname "$submodule_path")
26
336
 
27
- cd "$DEVCONTAINER_DIR"
337
+ echo " - ディレクトリをコピー中..."
338
+ docker exec "$container_name" rm -rf "${workspace_path}/${submodule_path}" 2>/dev/null || true
339
+ docker cp "$submodule_path" "${container_name}:${workspace_path}/${parent_dir}/"
28
340
 
29
- # devcontainer.jsonからinitializeCommandを読み取って実行(ホスト側で実行)
30
- INITIALIZE_CMD=$(jq -r '.initializeCommand // empty' "$DEVCONTAINER_JSON")
31
- if [ -n "$INITIALIZE_CMD" ]; then
32
- echo "Running initializeCommand..."
33
- eval "$INITIALIZE_CMD"
341
+ # node_modulesを削除し、所有権をnodeユーザーに変更
342
+ docker exec -u root "$container_name" rm -rf "${workspace_path}/${submodule_path}/node_modules" 2>/dev/null || true
343
+ docker exec -u root "$container_name" chown -R node:node "${workspace_path}/${submodule_path}" 2>/dev/null || true
344
+
345
+ if [[ $? -eq 0 ]]; then
346
+ echo -e " ${GREEN}OK $submodule_path コピー完了${NC}"
347
+ else
348
+ echo -e " ${RED}NG $submodule_path コピー失敗${NC}"
349
+ fi
350
+ else
351
+ echo -e " ${YELLOW}WARN $submodule_path が存在しません${NC}"
34
352
  fi
35
353
 
36
- # 終了時のcleanup関数
354
+ # .git/modules/下のgit情報のコピー
355
+ local git_modules_path=".git/modules/${submodule_path}"
356
+ if [[ -d "$git_modules_path" ]]; then
357
+ echo " - Git modules をコピー中..."
358
+
359
+ # コンテナ内のディレクトリを作成
360
+ local container_git_modules_parent
361
+ container_git_modules_parent=$(dirname "${workspace_path}/.git/modules/${submodule_path}")
362
+ docker exec "$container_name" mkdir -p "$container_git_modules_parent" 2>/dev/null
363
+
364
+ docker exec "$container_name" rm -rf "${workspace_path}/.git/modules/${submodule_path}" 2>/dev/null || true
365
+ docker cp "$git_modules_path" "${container_name}:${container_git_modules_parent}/"
366
+
367
+ if [[ $? -eq 0 ]]; then
368
+ echo -e " ${GREEN}OK .git/modules/${submodule_path} コピー完了${NC}"
369
+ else
370
+ echo -e " ${RED}NG .git/modules/${submodule_path} コピー失敗${NC}"
371
+ fi
372
+ else
373
+ echo -e " ${YELLOW}WARN .git/modules/${submodule_path} が存在しません${NC}"
374
+ fi
375
+
376
+ echo ""
377
+ done <<< "$submodules"
378
+
379
+ echo "サブモジュール同期が完了しました。"
380
+ }
381
+
382
+ # sync-submodules コマンドのメイン処理
383
+ cmd_sync_submodules() {
384
+ local target_container=""
385
+
386
+ # 引数解析
387
+ if [[ $# -eq 0 ]]; then
388
+ echo -e "${RED}Error: コンテナ名が指定されていません${NC}" >&2
389
+ echo "Usage: $(basename "$0") sync-submodules <container>" >&2
390
+ exit 1
391
+ fi
392
+
393
+ target_container="$1"
394
+
395
+ # Dockerの起動確認
396
+ check_docker false
397
+
398
+ # コンテナの存在確認
399
+ if ! docker ps --format "{{.Names}}" | grep -q "^${target_container}$"; then
400
+ echo -e "${RED}Error: コンテナ '$target_container' が見つかりません${NC}" >&2
401
+ exit 1
402
+ fi
403
+
404
+ sync_submodules_to_container "$target_container"
405
+ }
406
+
407
+ # ================================================
408
+ # exec サブコマンド用関数
409
+ # ================================================
410
+
411
+ cmd_exec() {
412
+ # containers --json で情報を取得
413
+ local json_output
414
+ json_output=$(cmd_containers --json 2>/dev/null)
415
+
416
+ if [ $? -ne 0 ]; then
417
+ echo "Error: Failed to get container information" >&2
418
+ exit 1
419
+ fi
420
+
421
+ # エラーチェック
422
+ if echo "$json_output" | grep -q '"error"'; then
423
+ local error_msg
424
+ error_msg=$(echo "$json_output" | grep -o '"error"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/"error"[[:space:]]*:[[:space:]]*"\([^"]*\)"/\1/')
425
+ echo "Error: $error_msg" >&2
426
+ exit 1
427
+ fi
428
+
429
+ # matched: true かつ available: true のコンテナをフィルタ
430
+ local available_containers
431
+ available_containers=$(echo "$json_output" | jq -r '.containers[] | select(.matched == true and .available == true) | .name')
432
+
433
+ # 空行を除いてカウント
434
+ local container_count
435
+ if [ -z "$available_containers" ]; then
436
+ container_count=0
437
+ else
438
+ container_count=$(echo "$available_containers" | wc -l | tr -d ' ')
439
+ fi
440
+
441
+ local selected_container
442
+ if [ "$container_count" -eq 0 ]; then
443
+ echo "Error: No available containers found for this project" >&2
444
+ echo "Please start a container first with: $(basename "$0") run" >&2
445
+ exit 1
446
+ elif [ "$container_count" -eq 1 ]; then
447
+ # 1つなら自動選択
448
+ selected_container="$available_containers"
449
+ else
450
+ # 複数ならselectメニューで選択
451
+ echo "Multiple containers available. Please select one:"
452
+ select selected_container in $available_containers; do
453
+ if [ -n "$selected_container" ]; then
454
+ break
455
+ fi
456
+ done
457
+ fi
458
+
459
+ # ワークスペースディレクトリを自動検出
460
+ local workspace_dir
461
+ workspace_dir=$(docker exec "$selected_container" bash -c 'ls -d /workspaces/*/ 2>/dev/null | head -1 | sed "s/\/$//"' 2>/dev/null || echo "")
462
+
463
+ if [ -z "$workspace_dir" ]; then
464
+ echo "Warning: Could not detect workspace directory, using root" >&2
465
+ workspace_dir="/"
466
+ fi
467
+
468
+ # 引数がなければエラー
469
+ if [ $# -eq 0 ]; then
470
+ echo "Error: No command specified" >&2
471
+ echo "Usage: $(basename "$0") exec <command>..." >&2
472
+ exit 1
473
+ fi
474
+
475
+ # コンテナ内でコマンドを実行
476
+ docker exec -it -w "$workspace_dir" "$selected_container" "$@"
477
+ }
478
+
479
+ # ================================================
480
+ # run サブコマンド用関数
481
+ # ================================================
482
+
483
+ cmd_run() {
484
+ # オプション解析
485
+ local detach_mode=false
486
+ while [ $# -gt 0 ]; do
487
+ case "$1" in
488
+ -d|--detach)
489
+ detach_mode=true
490
+ shift
491
+ ;;
492
+ *)
493
+ echo "Unknown option: $1" >&2
494
+ exit 1
495
+ ;;
496
+ esac
497
+ done
498
+
499
+ # プロジェクトルートをgitから取得
500
+ PROJECT_ROOT="$(git rev-parse --show-toplevel)"
501
+ export PROJECT_ROOT
502
+
503
+ DEVCONTAINER_DIR="$PROJECT_ROOT/.devcontainer"
504
+ DEVCONTAINER_JSON="$PROJECT_ROOT/.devcontainer/devcontainer.json"
505
+
506
+ # ランダムなプロジェクト名を生成(sandbox-XXXX形式)
507
+ PROJECT_NAME="sandbox-$(head -c 4 /dev/urandom | xxd -p)"
508
+ export PROJECT_NAME
509
+
510
+ # devcontainer.jsonからinitializeCommandを読み取って実行(ホスト側で実行)
511
+ INITIALIZE_CMD=$(jq -r '.initializeCommand // empty' "$DEVCONTAINER_JSON")
512
+ if [ -n "$INITIALIZE_CMD" ]; then
513
+ echo "Running initializeCommand..."
514
+ eval "$INITIALIZE_CMD"
515
+ fi
516
+
517
+ # 終了時のcleanup関数(detachモードでは設定しない)
518
+ if [ "$detach_mode" = false ]; then
37
519
  cleanup() {
38
520
  echo ""
39
521
  echo "Stopping Dev Container..."
40
- docker compose -p "$PROJECT_NAME" down -v --remove-orphans 2>/dev/null || true
522
+ docker compose -f "$DEVCONTAINER_DIR/docker-compose.yml" -p "$PROJECT_NAME" down -v --remove-orphans 2>/dev/null || true
41
523
  }
42
-
43
- # 終了時に必ずcleanupを実行
44
524
  trap cleanup EXIT
525
+ fi
526
+
527
+ echo "Starting Dev Container (project: $PROJECT_NAME)..."
528
+ docker compose -f "$DEVCONTAINER_DIR/docker-compose.yml" -p "$PROJECT_NAME" up -d
45
529
 
46
- echo "Starting Dev Container (project: $PROJECT_NAME)..."
47
- docker compose -p "$PROJECT_NAME" up -d
530
+ # コンテナが起動するまで待機
531
+ echo "Waiting for container to be ready..."
532
+ sleep 5
48
533
 
49
- # コンテナが起動するまで待機
50
- echo "Waiting for container to be ready..."
51
- sleep 5
534
+ # コンテナIDを取得
535
+ CONTAINER_ID=$(docker compose -f "$DEVCONTAINER_DIR/docker-compose.yml" -p "$PROJECT_NAME" ps -q devcontainer)
52
536
 
53
- # コンテナIDを取得
54
- CONTAINER_ID=$(docker compose -p "$PROJECT_NAME" ps -q devcontainer)
537
+ # devcontainer.jsonからコマンドを読み取って実行
538
+ POST_CREATE_CMD=$(jq -r '.postCreateCommand // empty' "$DEVCONTAINER_JSON")
539
+ POST_START_CMD=$(jq -r '.postStartCommand // empty' "$DEVCONTAINER_JSON")
55
540
 
56
- # devcontainer.jsonからコマンドを読み取って実行
57
- POST_CREATE_CMD=$(jq -r '.postCreateCommand // empty' "$DEVCONTAINER_JSON")
58
- POST_START_CMD=$(jq -r '.postStartCommand // empty' "$DEVCONTAINER_JSON")
541
+ # ワークディレクトリを設定
542
+ WORKSPACE_DIR="/workspaces/$PROJECT_NAME"
59
543
 
60
- # ワークディレクトリを設定
61
- WORKSPACE_DIR="/workspaces/$PROJECT_NAME"
544
+ if [ -n "$POST_CREATE_CMD" ]; then
545
+ # postCreateCommandを && で分割し、git checkoutまでとそれ以降に分ける
546
+ GIT_CMDS=""
547
+ OTHER_CMDS=""
548
+ checkout_found=false
549
+
550
+ # && で分割して処理
551
+ IFS='&' read -ra PARTS <<< "$POST_CREATE_CMD"
552
+ for part in "${PARTS[@]}"; do
553
+ # 空や&のみの部分をスキップ
554
+ cmd=$(echo "$part" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
555
+ [[ -z "$cmd" ]] && continue
556
+
557
+ if [[ "$checkout_found" == false ]]; then
558
+ # git checkoutが見つかるまではGIT_CMDSに追加
559
+ if [[ -n "$GIT_CMDS" ]]; then
560
+ GIT_CMDS="$GIT_CMDS && $cmd"
561
+ else
562
+ GIT_CMDS="$cmd"
563
+ fi
564
+ # git checkoutを含むコマンドを見つけたらフラグを立てる
565
+ if [[ "$cmd" == *"git checkout"* ]]; then
566
+ checkout_found=true
567
+ fi
568
+ else
569
+ # git checkout以降はOTHER_CMDSに追加
570
+ if [[ -n "$OTHER_CMDS" ]]; then
571
+ OTHER_CMDS="$OTHER_CMDS && $cmd"
572
+ else
573
+ OTHER_CMDS="$cmd"
574
+ fi
575
+ fi
576
+ done
577
+
578
+ # 1. Git系コマンドを実行(git checkoutまで)
579
+ if [[ -n "$GIT_CMDS" ]]; then
580
+ echo "Running postCreateCommand (git setup)..."
581
+ docker exec -w "$WORKSPACE_DIR" -e GIT_ORIGIN_URL=/host-project "$CONTAINER_ID" bash -c "$GIT_CMDS"
582
+ fi
62
583
 
63
- if [ -n "$POST_CREATE_CMD" ]; then
64
- echo "Running postCreateCommand..."
65
- # sandbox.sh経由の場合はGIT_ORIGIN_URLを/host-projectに設定
66
- docker exec -w "$WORKSPACE_DIR" -e GIT_ORIGIN_URL=/host-project "$CONTAINER_ID" bash -c "$POST_CREATE_CMD"
584
+ # 2. サブモジュールをホストからコンテナに同期
585
+ if [[ -f "$PROJECT_ROOT/.gitmodules" ]]; then
586
+ echo "Syncing submodules from host..."
587
+ sync_submodules_to_container "$CONTAINER_ID"
67
588
  fi
68
589
 
69
- if [ -n "$POST_START_CMD" ]; then
70
- echo "Running postStartCommand..."
71
- docker exec -w "$WORKSPACE_DIR" "$CONTAINER_ID" bash -c "$POST_START_CMD"
590
+ # 3. 残りのコマンドを実行(npm ci等)
591
+ if [[ -n "$OTHER_CMDS" ]]; then
592
+ # Sandbox環境ではgit submodule updateは不要(sync_submodules_to_containerで代替)
593
+ # git submodule updateコマンドを除外し、残った && を整理
594
+ OTHER_CMDS=$(echo "$OTHER_CMDS" | sed 's/git submodule update[^&;]*//g' | sed 's/&& &&/\&\&/g' | sed 's/^[[:space:]]*&&[[:space:]]*//;s/[[:space:]]*&&[[:space:]]*$//')
595
+ echo "Running postCreateCommand (setup)..."
596
+ docker exec -w "$WORKSPACE_DIR" -e GIT_ORIGIN_URL=/host-project "$CONTAINER_ID" bash -c "$OTHER_CMDS"
72
597
  fi
598
+ else
599
+ # postCreateCommandがない場合もサブモジュール同期は実行
600
+ if [[ -f "$PROJECT_ROOT/.gitmodules" ]]; then
601
+ echo "Syncing submodules from host..."
602
+ sync_submodules_to_container "$CONTAINER_ID"
603
+ fi
604
+ fi
605
+
606
+ if [ -n "$POST_START_CMD" ]; then
607
+ echo "Running postStartCommand..."
608
+ docker exec -w "$WORKSPACE_DIR" "$CONTAINER_ID" bash -c "$POST_START_CMD"
609
+ fi
610
+
611
+ # detachモードの場合はコンテナ情報を表示して終了
612
+ if [ "$detach_mode" = true ]; then
613
+ CONTAINER_NAME=$(docker compose -f "$DEVCONTAINER_DIR/docker-compose.yml" -p "$PROJECT_NAME" ps --format '{{.Names}}' devcontainer)
614
+ echo ""
615
+ echo "Container started in detach mode."
616
+ echo " Container: $CONTAINER_NAME"
617
+ echo " Workspace: $WORKSPACE_DIR"
618
+ echo ""
619
+ echo "To connect: $(basename "$0") exec bash"
620
+ echo "To stop: docker compose -p $PROJECT_NAME down -v"
621
+ exit 0
622
+ fi
623
+
624
+ # ランダムな色コードを生成(31-36: 赤、緑、黄、青、マゼンタ、シアン)
625
+ COLORS=(31 32 33 34 35 36)
626
+ RANDOM_COLOR=${COLORS[$RANDOM % ${#COLORS[@]}]}
627
+
628
+ # カスタムPS1を設定してbashを起動
629
+ echo "Connecting to Dev Container..."
630
+ docker exec -it -w "$WORKSPACE_DIR" "$CONTAINER_ID" bash -c "export PS1='\[\e[${RANDOM_COLOR}m\]\h\[\e[0m\]:\w# '; exec bash" || true
631
+ }
632
+
633
+ # ================================================
634
+ # 使い方表示
635
+ # ================================================
636
+
637
+ usage() {
638
+ cat << EOF
639
+ Usage: $(basename "$0") <command> [options]
640
+
641
+ DevContainerの管理とコマンド実行を行うスクリプト
73
642
 
74
- # ランダムな色コードを生成(31-36: 赤、緑、黄、青、マゼンタ、シアン)
75
- COLORS=(31 32 33 34 35 36)
76
- RANDOM_COLOR=${COLORS[$RANDOM % ${#COLORS[@]}]}
643
+ Commands:
644
+ init devcontainerファイルを初期化
645
+ run [-d|--detach] Dev Containerを起動 (デフォルト: インタラクティブ)
646
+ exec <cmd>... 起動中のコンテナでコマンドを実行
647
+ containers [--json] コンテナ一覧を表示
648
+ sync-submodules <container> 指定コンテナにサブモジュールを同期
77
649
 
78
- # カスタムPS1を設定してbashを起動
79
- echo "Connecting to Dev Container..."
80
- docker exec -it -w "$WORKSPACE_DIR" "$CONTAINER_ID" bash -c "export PS1='\[\e[${RANDOM_COLOR}m\]\h\[\e[0m\]:\w# '; exec bash" || true
650
+ Options:
651
+ run:
652
+ -d, --detach バックグラウンドで起動
653
+
654
+ containers:
655
+ --json JSON形式で出力
656
+
657
+ Examples:
658
+ $(basename "$0") run # インタラクティブモードで起動
659
+ $(basename "$0") run --detach # バックグラウンドで起動
660
+ $(basename "$0") exec bash # コンテナ内でbashを起動
661
+ $(basename "$0") containers # コンテナ一覧をテーブル表示
662
+ $(basename "$0") containers --json # コンテナ一覧をJSON出力
663
+ $(basename "$0") sync-submodules sandbox-xxx-1 # サブモジュール同期
664
+ EOF
665
+ exit 0
666
+ }
667
+
668
+ # ================================================
669
+ # メイン処理
670
+ # ================================================
671
+
672
+ COMMAND="${1:-}"
673
+
674
+ case "$COMMAND" in
675
+ init)
676
+ node "$SCRIPT_DIR/postinstall.js"
677
+ ;;
678
+ exec)
679
+ shift
680
+ cmd_exec "$@"
681
+ ;;
682
+ run)
683
+ shift
684
+ cmd_run "$@"
685
+ ;;
686
+ containers)
687
+ shift
688
+ cmd_containers "$@"
689
+ ;;
690
+ sync-submodules)
691
+ shift
692
+ cmd_sync_submodules "$@"
693
+ ;;
694
+ --help|-h|help)
695
+ usage
696
+ ;;
697
+ "")
698
+ usage
81
699
  ;;
82
700
  *)
83
- echo "Usage: $(basename "$0") [init|run]"
84
- echo " init - Initialize devcontainer files"
85
- echo " run - Start Dev Container (default)"
86
- exit 1
701
+ echo "Unknown command: $COMMAND" >&2
702
+ echo ""
703
+ usage
87
704
  ;;
88
705
  esac
@@ -1,18 +0,0 @@
1
- {
2
- "service": "devcontainer",
3
- "dockerComposeFile": [
4
- "docker-compose.yml"
5
- ],
6
- "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
7
- "workspaceMount": "",
8
- "updateRemoteUserUID": true,
9
- "initializeCommand": "echo 'PROJECT_ROOT='$(pwd) > .devcontainer/.env && echo 'PROJECT_NAME='$(basename $(pwd)) >> .devcontainer/.env && echo 'GIT_ORIGIN_URL='$(git remote get-url origin 2>/dev/null || echo '') >> .devcontainer/.env && [ -f ~/.claude-sandbox-credentials.json ] || echo '{}' > ~/.claude-sandbox-credentials.json && [ -f ~/.claude-sandbox.json ] || echo '{}' > ~/.claude-sandbox.json && [ -d ~/.claude-test-nonexistent/plugins ] || mkdir -p ~/.claude-test-nonexistent/plugins",
10
- "postCreateCommand": "git init && git remote add origin \"${GIT_ORIGIN_URL:-/host-project}\" && git fetch && git checkout $(git -C /host-project branch --show-current) && echo 'alias claude=\"npx claude --dangerously-skip-permissions\"' >> ~/.bashrc",
11
- "postStartCommand": "cp -rT /home/node/.claude-host-plugins /home/node/.claude/plugins 2>/dev/null || true; [ -f /home/node/.claude/plugins/known_marketplaces.json ] && sed -i 's|/Users/[^/]*/|/home/node/|g' /home/node/.claude/plugins/known_marketplaces.json; [ -f /home/node/.claude/plugins/installed_plugins.json ] && sed -i 's|/Users/[^/]*/|/home/node/|g' /home/node/.claude/plugins/installed_plugins.json; true",
12
- "shutdownAction": "stopCompose",
13
- "remoteUser": "node",
14
- "remoteEnv": {
15
- "LANG": "ja_JP.UTF-8",
16
- "LC_ALL": "ja_JP.UTF-8"
17
- }
18
- }
@@ -1,30 +0,0 @@
1
- {
2
- "name": "sandbox",
3
- "dockerComposeFile": ["docker-compose.override.yml"],
4
- "postCreateCommand": "npm ci",
5
- "forwardPorts": [3000, 3306],
6
- "portsAttributes": {
7
- "3000": {
8
- "label": "Frontend",
9
- "onAutoForward": "notify"
10
- },
11
- "3306": {
12
- "label": "Database",
13
- "onAutoForward": "silent"
14
- }
15
- },
16
- "customizations": {
17
- "vscode": {
18
- "extensions": ["Prisma.prisma"],
19
- "settings": {
20
- "[prisma]": {
21
- "editor.defaultFormatter": "Prisma.prisma"
22
- }
23
- }
24
- }
25
- },
26
- "remoteEnv": {
27
- "DATABASE_URL": "mysql://root:password@db:3306/sandbox",
28
- "NODE_ENV": "development"
29
- }
30
- }
@@ -1,24 +0,0 @@
1
- services:
2
- devcontainer:
3
- depends_on:
4
- db:
5
- condition: service_healthy
6
- environment:
7
- DATABASE_URL: mysql://root:password@db:3306/sandbox
8
- NODE_ENV: development
9
-
10
- db:
11
- image: mysql:8.0
12
- environment:
13
- MYSQL_ROOT_PASSWORD: password
14
- MYSQL_DATABASE: "sandbox"
15
- healthcheck:
16
- test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
17
- interval: 10s
18
- timeout: 5s
19
- retries: 5
20
- volumes:
21
- - db_data:/var/lib/mysql
22
-
23
- volumes:
24
- db_data: