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.
- package/.devcontainer/Dockerfile +3 -2
- package/.devcontainer/devcontainer.base.json +1 -1
- package/.devcontainer/docker-compose.yml +3 -3
- package/package.json +1 -1
- package/scripts/sandbox.sh +670 -53
- package/.devcontainer/devcontainer.json +0 -18
- package/.devcontainer/sample.devcontainer.override.json +0 -30
- package/.devcontainer/sample.docker-compose.override.yml +0 -24
package/.devcontainer/Dockerfile
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
28
|
+
GIT_ORIGIN_URL: ${GIT_ORIGIN_URL}
|
package/package.json
CHANGED
package/scripts/sandbox.sh
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
DEVCONTAINER_JSON="$PROJECT_ROOT/.devcontainer/devcontainer.json"
|
|
330
|
+
echo "### $submodule_path"
|
|
22
331
|
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
332
|
+
# サブモジュールディレクトリのコピー
|
|
333
|
+
if [[ -d "$submodule_path" ]]; then
|
|
334
|
+
local parent_dir
|
|
335
|
+
parent_dir=$(dirname "$submodule_path")
|
|
26
336
|
|
|
27
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
47
|
-
|
|
530
|
+
# コンテナが起動するまで待機
|
|
531
|
+
echo "Waiting for container to be ready..."
|
|
532
|
+
sleep 5
|
|
48
533
|
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
POST_START_CMD=$(jq -r '.postStartCommand // empty' "$DEVCONTAINER_JSON")
|
|
541
|
+
# ワークディレクトリを設定
|
|
542
|
+
WORKSPACE_DIR="/workspaces/$PROJECT_NAME"
|
|
59
543
|
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
643
|
+
Commands:
|
|
644
|
+
init devcontainerファイルを初期化
|
|
645
|
+
run [-d|--detach] Dev Containerを起動 (デフォルト: インタラクティブ)
|
|
646
|
+
exec <cmd>... 起動中のコンテナでコマンドを実行
|
|
647
|
+
containers [--json] コンテナ一覧を表示
|
|
648
|
+
sync-submodules <container> 指定コンテナにサブモジュールを同期
|
|
77
649
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 "
|
|
84
|
-
echo "
|
|
85
|
-
|
|
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:
|