sanduary 1.0.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.
@@ -0,0 +1,72 @@
1
+ # Dev Container用Dockerfile
2
+ # Node.js 22 + Docker CLI + Playwright
3
+
4
+ FROM node:22-bookworm
5
+
6
+ # Docker CLI インストール(ホストのDockerを操作するため)
7
+ RUN apt-get update && apt-get install -y \
8
+ ca-certificates \
9
+ curl \
10
+ gnupg \
11
+ lsb-release \
12
+ && mkdir -p /etc/apt/keyrings \
13
+ && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
14
+ && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \
15
+ && apt-get update \
16
+ && apt-get install -y docker-ce-cli docker-compose-plugin \
17
+ && apt-get clean \
18
+ && rm -rf /var/lib/apt/lists/*
19
+
20
+ # 開発に便利なツール
21
+ RUN apt-get update && apt-get install -y \
22
+ git \
23
+ vim \
24
+ jq \
25
+ zip \
26
+ less \
27
+ && apt-get clean \
28
+ && rm -rf /var/lib/apt/lists/*
29
+
30
+ # ロケール設定(マルチバイト文字対応)
31
+ RUN apt-get update && apt-get install -y \
32
+ locales \
33
+ && sed -i -e 's/# ja_JP.UTF-8 UTF-8/ja_JP.UTF-8 UTF-8/' /etc/locale.gen \
34
+ && sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \
35
+ && locale-gen \
36
+ && apt-get clean \
37
+ && rm -rf /var/lib/apt/lists/*
38
+
39
+ ENV LANG=ja_JP.UTF-8
40
+
41
+ # Playwright依存ライブラリ
42
+ RUN apt-get update && apt-get install -y \
43
+ libatk1.0-0 \
44
+ libatk-bridge2.0-0 \
45
+ libcups2 \
46
+ libxkbcommon0 \
47
+ libatspi2.0-0 \
48
+ libxcomposite1 \
49
+ libxdamage1 \
50
+ libxfixes3 \
51
+ libxrandr2 \
52
+ libgbm1 \
53
+ libasound2 \
54
+ libnspr4 \
55
+ libnss3 \
56
+ && apt-get clean \
57
+ && rm -rf /var/lib/apt/lists/*
58
+
59
+ # グローバルnpmパッケージ
60
+ RUN npm install -g npm@10.9.0
61
+
62
+ # プロジェクト名のビルド引数(デフォルト: project)
63
+ ARG PROJECT_NAME=project
64
+
65
+ # ワークスペースディレクトリをnodeユーザーの所有で作成
66
+ RUN mkdir -p /workspaces/${PROJECT_NAME} && chown node:node /workspaces/${PROJECT_NAME}
67
+
68
+ # 作業ディレクトリ
69
+ WORKDIR /workspaces/${PROJECT_NAME}
70
+
71
+ # nodeユーザーに切り替え
72
+ USER node
@@ -0,0 +1,15 @@
1
+ {
2
+ "service": "devcontainer",
3
+ "dockerComposeFile": ["docker-compose.yml"],
4
+ "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
5
+ "workspaceMount": "",
6
+ "updateRemoteUserUID": true,
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",
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",
9
+ "shutdownAction": "stopCompose",
10
+ "remoteUser": "node",
11
+ "remoteEnv": {
12
+ "LANG": "ja_JP.UTF-8",
13
+ "LC_ALL": "ja_JP.UTF-8"
14
+ }
15
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "service": "devcontainer",
3
+ "dockerComposeFile": [
4
+ "docker-compose.yml"
5
+ ],
6
+ "workspaceFolder": "/workspaces/${PROJECT_NAME:-project}",
7
+ "workspaceMount": "",
8
+ "updateRemoteUserUID": true,
9
+ "initializeCommand": "echo 'PROJECT_ROOT='$(pwd) > .devcontainer/.env && echo 'PROJECT_NAME='$(basename $(pwd)) >> .devcontainer/.env && [ -f ~/.claude-sandbox-credentials.json ] || echo '{}' > ~/.claude-sandbox-credentials.json && [ -f ~/.claude-sandbox.json ] || echo '{}' > ~/.claude-sandbox.json",
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
+ "shutdownAction": "stopCompose",
12
+ "remoteUser": "node",
13
+ "remoteEnv": {
14
+ "LANG": "ja_JP.UTF-8",
15
+ "LC_ALL": "ja_JP.UTF-8"
16
+ }
17
+ }
@@ -0,0 +1,26 @@
1
+ services:
2
+ devcontainer:
3
+ platform: linux/amd64
4
+ build:
5
+ context: .
6
+ dockerfile: Dockerfile
7
+ args:
8
+ PROJECT_NAME: ${PROJECT_NAME:-project}
9
+ group_add:
10
+ - ${DOCKER_GID:-999}
11
+ volumes:
12
+ - /var/run/docker.sock:/var/run/docker.sock
13
+ - ${PROJECT_ROOT:-..}:/host-project
14
+ - ${HOME}/.claude:/home/node/.claude
15
+ - ${HOME}/.claude-sandbox.json:/home/node/.claude.json
16
+ - ${HOME}/.claude-sandbox-credentials.json:/home/node/.claude/.credentials.json
17
+ - ${HOME}/.gitconfig:/home/node/.gitconfig
18
+ working_dir: /workspaces/${PROJECT_NAME:-project}
19
+ command: sleep infinity
20
+ environment:
21
+ LANG: ja_JP.UTF-8
22
+ LC_ALL: ja_JP.UTF-8
23
+ GIT_CONFIG_COUNT: 1
24
+ GIT_CONFIG_KEY_0: safe.directory
25
+ GIT_CONFIG_VALUE_0: /workspaces/${PROJECT_NAME:-project}
26
+ GIT_ORIGIN_URL: ${GIT_ORIGIN_URL:-/host-project}
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "sandbox",
3
+ "dockerComposeFile": ["docker-compose.override.yml"],
4
+ "postCreateCommand": "npm ci && npx prisma generate",
5
+ "postStartCommand": "npx prisma migrate deploy",
6
+ "forwardPorts": [3000, 3306],
7
+ "portsAttributes": {
8
+ "3000": {
9
+ "label": "Frontend",
10
+ "onAutoForward": "notify"
11
+ },
12
+ "3306": {
13
+ "label": "Database",
14
+ "onAutoForward": "silent"
15
+ }
16
+ },
17
+ "customizations": {
18
+ "vscode": {
19
+ "extensions": ["Prisma.prisma"],
20
+ "settings": {
21
+ "[prisma]": {
22
+ "editor.defaultFormatter": "Prisma.prisma"
23
+ }
24
+ }
25
+ }
26
+ },
27
+ "remoteEnv": {
28
+ "DATABASE_URL": "mysql://root:password@db:3306/sandbox",
29
+ "NODE_ENV": "development"
30
+ }
31
+ }
@@ -0,0 +1,24 @@
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:
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "{{PROJECT_NAME}}",
3
+ "dockerComposeFile": ["docker-compose.override.yml"],
4
+ "postCreateCommand": "npm ci && npx prisma generate",
5
+ "postStartCommand": "npx prisma migrate deploy",
6
+ "forwardPorts": [3000, 3306],
7
+ "portsAttributes": {
8
+ "3000": {
9
+ "label": "Frontend",
10
+ "onAutoForward": "notify"
11
+ },
12
+ "3306": {
13
+ "label": "Database",
14
+ "onAutoForward": "silent"
15
+ }
16
+ },
17
+ "customizations": {
18
+ "vscode": {
19
+ "extensions": ["Prisma.prisma"],
20
+ "settings": {
21
+ "[prisma]": {
22
+ "editor.defaultFormatter": "Prisma.prisma"
23
+ }
24
+ }
25
+ }
26
+ },
27
+ "remoteEnv": {
28
+ "DATABASE_URL": "mysql://root:password@db:3306/{{PROJECT_NAME}}",
29
+ "NODE_ENV": "development"
30
+ }
31
+ }
@@ -0,0 +1,24 @@
1
+ services:
2
+ devcontainer:
3
+ depends_on:
4
+ db:
5
+ condition: service_healthy
6
+ environment:
7
+ DATABASE_URL: mysql://root:password@db:3306/{{PROJECT_NAME}}
8
+ NODE_ENV: development
9
+
10
+ db:
11
+ image: mysql:8.0
12
+ environment:
13
+ MYSQL_ROOT_PASSWORD: password
14
+ MYSQL_DATABASE: "{{PROJECT_NAME}}"
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:
package/README.md ADDED
@@ -0,0 +1,289 @@
1
+ # sanduary
2
+
3
+ A secure development sandbox environment for AI agents - providing safe, isolated workspaces for AI coding assistants like Claude Code through Docker DevContainers.
4
+
5
+ ## Overview
6
+
7
+ **sanduary** (sanctuary + sandbox) creates disposable, secure development environments where AI agents can safely execute code, install packages, and perform development tasks without affecting your host system. Each session runs in an isolated Docker container that is automatically cleaned up when you exit.
8
+
9
+ ## Features
10
+
11
+ - **Isolated Environments**: Each session runs in a dedicated Docker DevContainer
12
+ - **Dynamic Configuration**: Supports project-specific DevContainer overrides
13
+ - **Auto-Cleanup**: Containers are automatically removed after session ends
14
+ - **Git Integration**: Seamlessly works with your existing Git repositories
15
+ - **Customizable**: Extend base configuration with override files
16
+ - **Vulnerability Reduction**: Generate DevContainer files on-demand instead of committing to Git
17
+
18
+ ## Installation
19
+
20
+ ### Global Installation (Recommended)
21
+
22
+ Install globally to use across multiple projects:
23
+
24
+ ```bash
25
+ npm install -g sanduary
26
+ ```
27
+
28
+ This allows any team member to generate their own DevContainer configuration without tracking potentially vulnerable configuration files in Git.
29
+
30
+ ### Project-Local Installation
31
+
32
+ For advanced use cases, install as a project dependency:
33
+
34
+ ```bash
35
+ npm install --save-dev sanduary
36
+ ```
37
+
38
+ Then add to your `package.json`:
39
+
40
+ ```json
41
+ {
42
+ "scripts": {
43
+ "postinstall": "sdy init"
44
+ }
45
+ }
46
+ ```
47
+
48
+ This automatically sets up the sandbox environment when developers run `npm install`, providing quick DevContainer setup without committing configuration files.
49
+
50
+ ## Quick Start
51
+
52
+ ### 1. Initialize DevContainer Configuration
53
+
54
+ Navigate to your project directory and initialize the DevContainer files:
55
+
56
+ ```bash
57
+ cd your-project
58
+ sdy init
59
+ ```
60
+
61
+ This creates `.devcontainer/` directory with base configuration files.
62
+
63
+ **Important**: Add `.devcontainer/` to your `.gitignore` to keep configuration local:
64
+
65
+ ```gitignore
66
+ # DevContainer - generated by sanduary
67
+ .devcontainer/
68
+ ```
69
+
70
+ ### 2. Start the Sandbox Environment
71
+
72
+ Launch the DevContainer (default command):
73
+
74
+ ```bash
75
+ sdy run
76
+ # or simply
77
+ sdy
78
+ ```
79
+
80
+ The container will:
81
+ - Build and start automatically
82
+ - Execute any `postCreateCommand` and `postStartCommand` from `devcontainer.json`
83
+ - Connect you to an interactive bash session
84
+ - Clean up automatically when you exit
85
+
86
+ ## Commands
87
+
88
+ | Command | Description |
89
+ |---------|-------------|
90
+ | `sdy init` | Initialize DevContainer configuration files in current project |
91
+ | `sdy run` | Start the DevContainer sandbox (default) |
92
+ | `sdy` | Alias for `sdy run` |
93
+
94
+ ## Configuration
95
+
96
+ ### Base Configuration
97
+
98
+ After running `sdy init`, you'll have:
99
+
100
+ ```
101
+ .devcontainer/
102
+ ├── devcontainer.base.json # Base DevContainer settings
103
+ ├── devcontainer.json # Main configuration file
104
+ ├── docker-compose.yml # Docker Compose settings
105
+ ├── Dockerfile # Container image definition
106
+ └── templates/
107
+ ├── devcontainer.override.template.json
108
+ └── docker-compose.override.template.yml
109
+ ```
110
+
111
+ **Note**: These files should NOT be committed to Git. Each developer generates their own configuration via `sdy init`.
112
+
113
+ ### Override Files
114
+
115
+ Customize your sandbox environment by creating override files:
116
+
117
+ #### DevContainer Override
118
+
119
+ Create `.devcontainer/devcontainer.override.json`:
120
+
121
+ ```json
122
+ {
123
+ "customizations": {
124
+ "vscode": {
125
+ "extensions": [
126
+ "dbaeumer.vscode-eslint",
127
+ "esbenp.prettier-vscode"
128
+ ]
129
+ }
130
+ },
131
+ "postCreateCommand": "npm install"
132
+ }
133
+ ```
134
+
135
+ #### Docker Compose Override
136
+
137
+ Create `.devcontainer/docker-compose.override.yml`:
138
+
139
+ ```yaml
140
+ services:
141
+ devcontainer:
142
+ environment:
143
+ - NODE_ENV=development
144
+ ports:
145
+ - "3000:3000"
146
+ volumes:
147
+ - ./custom-data:/data
148
+ ```
149
+
150
+ ### Configuration Files
151
+
152
+ The following files are automatically created in your home directory:
153
+
154
+ - `~/.claude-sandbox.json` - General sandbox settings
155
+ - `~/.claude-sandbox-credentials.json` - Authentication credentials
156
+
157
+ ## Git Management Best Practices
158
+
159
+ ### What to Commit
160
+
161
+ ✅ **DO commit**:
162
+ - `package.json` (with sanduary as dependency)
163
+ - `.gitignore` (with `.devcontainer/` excluded)
164
+ - Project source code and assets
165
+
166
+ ### What NOT to Commit
167
+
168
+ ❌ **DO NOT commit**:
169
+ - `.devcontainer/` directory and its contents
170
+ - `devcontainer.json`
171
+ - `docker-compose.yml`
172
+ - `Dockerfile`
173
+
174
+ ### Why?
175
+
176
+ 1. **Security**: DevContainer configurations can contain sensitive settings or expose vulnerabilities
177
+ 2. **Flexibility**: Each developer can customize their environment without affecting others
178
+ 3. **Version Control**: Configuration generation is handled by sanduary versions, not Git history
179
+
180
+ ### Sample `.gitignore`
181
+
182
+ ```gitignore
183
+ # DevContainer - generated by sanduary
184
+ .devcontainer/
185
+
186
+ # Dependency directories
187
+ node_modules/
188
+ ```
189
+
190
+ ## Workflow Examples
191
+
192
+ ### Team Collaboration
193
+
194
+ 1. **Project Setup** (once per project):
195
+ ```bash
196
+ # Add sanduary to project
197
+ npm install --save-dev sanduary
198
+
199
+ # Update .gitignore
200
+ echo ".devcontainer/" >> .gitignore
201
+
202
+ # Commit
203
+ git add package.json .gitignore
204
+ git commit -m "feat: add sanduary for DevContainer management"
205
+ ```
206
+
207
+ 2. **New Developer Setup**:
208
+ ```bash
209
+ # Clone project
210
+ git clone <repo-url>
211
+ cd <project>
212
+
213
+ # Install dependencies (automatically runs sdy init via postinstall)
214
+ npm install
215
+
216
+ # Start sandbox
217
+ sdy
218
+ ```
219
+
220
+ 3. **Existing Developer**:
221
+ ```bash
222
+ # Pull latest changes
223
+ git pull
224
+
225
+ # Update dependencies if needed
226
+ npm install
227
+
228
+ # Start sandbox
229
+ sdy
230
+ ```
231
+
232
+ ### Global Installation Workflow
233
+
234
+ 1. **One-time Setup**:
235
+ ```bash
236
+ # Install globally
237
+ npm install -g sanduary
238
+ ```
239
+
240
+ 2. **Per-project Usage**:
241
+ ```bash
242
+ cd your-project
243
+
244
+ # Initialize (only needed once per project)
245
+ sdy init
246
+
247
+ # Start sandbox (anytime)
248
+ sdy
249
+ ```
250
+
251
+ ## How It Works
252
+
253
+ 1. **Dynamic Naming**: Each session gets a unique project name (`sandbox-XXXX`)
254
+ 2. **Git-Aware**: Automatically detects project root via Git
255
+ 3. **Docker Compose**: Uses Docker Compose for container orchestration
256
+ 4. **Lifecycle Hooks**: Executes `postCreateCommand` and `postStartCommand` from DevContainer config
257
+ 5. **Auto-Cleanup**: Containers and volumes are removed on exit via cleanup trap
258
+
259
+ ## Use Cases
260
+
261
+ - **AI Agent Sandboxing**: Safe environment for Claude Code and similar AI assistants
262
+ - **Dependency Testing**: Test package installations without polluting host
263
+ - **Code Experimentation**: Try risky changes in isolated environment
264
+ - **Multi-Project Development**: Switch between different project configurations easily
265
+ - **Onboarding**: New team members get consistent development environments instantly
266
+
267
+ ## Requirements
268
+
269
+ - Docker Engine
270
+ - Node.js and npm (for installation)
271
+ - Git (for project detection)
272
+ - `jq` (for JSON parsing)
273
+
274
+ ## Environment Variables
275
+
276
+ The following environment variables are automatically set during execution:
277
+
278
+ - `PROJECT_ROOT`: Git repository root directory
279
+ - `PROJECT_NAME`: Unique sandbox instance name
280
+ - `GIT_ORIGIN_URL`: Set to `/host-project` when launched via `sdy`
281
+
282
+ ## License
283
+
284
+ ISC
285
+
286
+ ## Links
287
+
288
+ - [GitHub Repository](https://github.com/yamadamasahiro/sanduary)
289
+ - [Report Issues](https://github.com/yamadamasahiro/sanduary/issues)
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "sanduary",
3
+ "version": "1.0.0",
4
+ "description": "Development sandbox environment for AI agents - A secure sanctuary for running AI coding assistants in Docker DevContainers",
5
+ "keywords": [
6
+ "devcontainer",
7
+ "docker",
8
+ "sandbox",
9
+ "ai",
10
+ "agent",
11
+ "claude",
12
+ "development",
13
+ "cli"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/yamadamasahiro/sanduary.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/yamadamasahiro/sanduary/issues"
21
+ },
22
+ "homepage": "https://github.com/yamadamasahiro/sanduary#readme",
23
+ "scripts": {},
24
+ "bin": {
25
+ "sdy": "scripts/sandbox.sh"
26
+ },
27
+ "files": [
28
+ "scripts/",
29
+ ".devcontainer/"
30
+ ],
31
+ "dependencies": {
32
+ "js-yaml": "^4.1.0"
33
+ },
34
+ "main": "index.js",
35
+ "devDependencies": {
36
+ "@anthropic-ai/claude-code": "^2.0.72"
37
+ },
38
+ "author": "",
39
+ "license": "ISC"
40
+ }
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs')
3
+ const path = require('path')
4
+ const yaml = require('js-yaml')
5
+
6
+ const INIT_CWD = process.env.INIT_CWD || process.cwd()
7
+ const SANDBOX_DEVCONTAINER_PATH =
8
+ '../packages/infrastructure/sandbox/.devcontainer'
9
+
10
+ // 連結すべきフィールド(deepMergeではなく && で連結)
11
+ const CONCAT_FIELDS = ['postCreateCommand', 'postStartCommand']
12
+
13
+ // テンプレートディレクトリ
14
+ const templatesDir = path.join(__dirname, '../.devcontainer/templates')
15
+
16
+ // パス
17
+ const devcontainerDir = path.join(INIT_CWD, '.devcontainer')
18
+ const baseJsonPath = path.join(
19
+ __dirname,
20
+ '../.devcontainer/devcontainer.base.json',
21
+ )
22
+ const overrideJsonPath = path.join(
23
+ devcontainerDir,
24
+ 'devcontainer.override.json',
25
+ )
26
+ const outputJsonPath = path.join(devcontainerDir, 'devcontainer.json')
27
+
28
+ // Docker Compose パス
29
+ const baseComposePath = path.join(
30
+ __dirname,
31
+ '../.devcontainer/docker-compose.yml',
32
+ )
33
+ const overrideComposePath = path.join(
34
+ devcontainerDir,
35
+ 'docker-compose.override.yml',
36
+ )
37
+ const outputComposePath = path.join(devcontainerDir, 'docker-compose.yml')
38
+
39
+ // Dockerfile パス
40
+ const sourceDockerfilePath = path.join(__dirname, '../.devcontainer/Dockerfile')
41
+ const outputDockerfilePath = path.join(devcontainerDir, 'Dockerfile')
42
+
43
+ // ディレクトリ作成
44
+ fs.mkdirSync(devcontainerDir, { recursive: true })
45
+
46
+ /**
47
+ * 深いマージを行う関数
48
+ * - 配列はconcatで結合
49
+ * - オブジェクトは再帰的にマージ
50
+ * - プリミティブ値はoverrideが優先
51
+ */
52
+ function deepMerge(base, override) {
53
+ if (override === undefined) {
54
+ return base
55
+ }
56
+ if (base === undefined) {
57
+ return override
58
+ }
59
+
60
+ // 両方が配列の場合はconcat
61
+ if (Array.isArray(base) && Array.isArray(override)) {
62
+ return [...base, ...override]
63
+ }
64
+
65
+ // 両方がオブジェクトの場合は再帰マージ
66
+ if (
67
+ typeof base === 'object' &&
68
+ base !== null &&
69
+ typeof override === 'object' &&
70
+ override !== null &&
71
+ !Array.isArray(base) &&
72
+ !Array.isArray(override)
73
+ ) {
74
+ const result = { ...base }
75
+ for (const key of Object.keys(override)) {
76
+ result[key] = deepMerge(base[key], override[key])
77
+ }
78
+ return result
79
+ }
80
+
81
+ // それ以外はoverrideが優先
82
+ return override
83
+ }
84
+
85
+ /**
86
+ * 特定のフィールドを連結する関数
87
+ * CONCAT_FIELDSに指定されたフィールドは && で連結
88
+ */
89
+ function applyConcatFields(merged, base, override) {
90
+ for (const field of CONCAT_FIELDS) {
91
+ if (base[field] && override[field]) {
92
+ merged[field] = `${base[field]} && ${override[field]}`
93
+ }
94
+ }
95
+ return merged
96
+ }
97
+
98
+ /**
99
+ * テンプレートからファイルを作成する関数
100
+ * PROJECT_NAMEプレースホルダーを置換
101
+ */
102
+ function createFromTemplate(templatePath, outputPath) {
103
+ if (fs.existsSync(templatePath)) {
104
+ const projectName = path.basename(INIT_CWD)
105
+ let templateContent = fs.readFileSync(templatePath, 'utf8')
106
+ templateContent = templateContent.replace(/\{\{PROJECT_NAME\}\}/g, projectName)
107
+ fs.writeFileSync(outputPath, templateContent)
108
+ console.log(`Created from template: ${outputPath}`)
109
+ return true
110
+ }
111
+ return false
112
+ }
113
+
114
+ // =====================================
115
+ // devcontainer.json の生成
116
+ // =====================================
117
+
118
+ // base.jsonを読み込み
119
+ const base = JSON.parse(fs.readFileSync(baseJsonPath, 'utf8'))
120
+
121
+ // overrideファイルが存在しない場合、サンプルテンプレートを作成(sample.接頭辞付き)
122
+ const overrideJsonTemplate = path.join(templatesDir, 'devcontainer.override.template.json')
123
+ const sampleOverrideJsonPath = path.join(devcontainerDir, 'sample.devcontainer.override.json')
124
+ if (!fs.existsSync(overrideJsonPath) && !fs.existsSync(sampleOverrideJsonPath)) {
125
+ createFromTemplate(overrideJsonTemplate, sampleOverrideJsonPath)
126
+ }
127
+
128
+ // override.jsonを読み込み(存在しない場合は空オブジェクト)
129
+ let override = {}
130
+ if (fs.existsSync(overrideJsonPath)) {
131
+ override = JSON.parse(fs.readFileSync(overrideJsonPath, 'utf8'))
132
+ }
133
+
134
+ // dockerComposeFileをマージ
135
+ const baseDockerComposeFiles = (base.dockerComposeFile || []).map(
136
+ (file) => `${SANDBOX_DEVCONTAINER_PATH}/${file}`,
137
+ )
138
+ const overrideDockerComposeFiles = override.dockerComposeFile || []
139
+
140
+ // オブジェクトをマージ(override が優先、深いマージで remoteEnv 等も正しくマージ)
141
+ const merged = deepMerge(base, override)
142
+
143
+ // CONCAT_FIELDSは && で連結(deepMergeではoverrideが優先されるため、ここで連結処理)
144
+ applyConcatFields(merged, base, override)
145
+
146
+ // dockerComposeFileは配列を結合(ただしdocker-compose.ymlを生成するのでそれを参照)
147
+ merged.dockerComposeFile = ['docker-compose.yml']
148
+
149
+ // 出力
150
+ fs.writeFileSync(outputJsonPath, JSON.stringify(merged, null, 2) + '\n')
151
+
152
+ console.log(`Generated: ${outputJsonPath}`)
153
+
154
+ // =====================================
155
+ // docker-compose.yml の生成
156
+ // =====================================
157
+
158
+ // base docker-compose.ymlを読み込み
159
+ const baseComposeContent = fs.readFileSync(baseComposePath, 'utf8')
160
+ const baseCompose = yaml.load(baseComposeContent)
161
+
162
+ // overrideファイルが存在しない場合、サンプルテンプレートを作成(sample.接頭辞付き)
163
+ const overrideComposeTemplate = path.join(templatesDir, 'docker-compose.override.template.yml')
164
+ const sampleOverrideComposePath = path.join(devcontainerDir, 'sample.docker-compose.override.yml')
165
+ if (!fs.existsSync(overrideComposePath) && !fs.existsSync(sampleOverrideComposePath)) {
166
+ createFromTemplate(overrideComposeTemplate, sampleOverrideComposePath)
167
+ }
168
+
169
+ // override docker-compose.ymlを読み込み(存在しない場合は空オブジェクト)
170
+ let overrideCompose = {}
171
+ if (fs.existsSync(overrideComposePath)) {
172
+ const overrideComposeContent = fs.readFileSync(overrideComposePath, 'utf8')
173
+ overrideCompose = yaml.load(overrideComposeContent)
174
+ }
175
+
176
+ // 深いマージを実行
177
+ const mergedCompose = deepMerge(baseCompose, overrideCompose)
178
+
179
+ // build.contextとbuild.dockerfileのパスを調整(ローカルのDockerfileを参照)
180
+ if (
181
+ mergedCompose.services &&
182
+ mergedCompose.services.devcontainer &&
183
+ mergedCompose.services.devcontainer.build
184
+ ) {
185
+ mergedCompose.services.devcontainer.build.context = '.'
186
+ mergedCompose.services.devcontainer.build.dockerfile = 'Dockerfile'
187
+ }
188
+
189
+ // YAMLとして出力(変数展開を維持するため、quotingOptionsを設定)
190
+ const outputYaml = yaml.dump(mergedCompose, {
191
+ lineWidth: -1, // 行の折り返しを無効化
192
+ quotingType: '"',
193
+ forceQuotes: false,
194
+ noRefs: true,
195
+ })
196
+
197
+ fs.writeFileSync(outputComposePath, outputYaml)
198
+
199
+ console.log(`Generated: ${outputComposePath}`)
200
+
201
+ // =====================================
202
+ // Dockerfile のコピー
203
+ // =====================================
204
+
205
+ fs.copyFileSync(sourceDockerfilePath, outputDockerfilePath)
206
+
207
+ console.log(`Copied: ${outputDockerfilePath}`)
@@ -0,0 +1,93 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # スクリプトの実際のパスを取得(シンボリックリンクを解決)
5
+ SCRIPT_PATH="$(realpath "$0")"
6
+ SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
7
+ PACKAGE_ROOT="$(dirname "$SCRIPT_DIR")"
8
+
9
+ COMMAND="${1:-run}"
10
+
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
19
+
20
+ DEVCONTAINER_DIR="$PROJECT_ROOT/.devcontainer"
21
+ DEVCONTAINER_JSON="$PROJECT_ROOT/.devcontainer/devcontainer.json"
22
+
23
+ # ランダムなプロジェクト名を生成(sandbox-XXXX形式)
24
+ PROJECT_NAME="sandbox-$(head -c 4 /dev/urandom | xxd -p)"
25
+ export PROJECT_NAME
26
+
27
+ cd "$DEVCONTAINER_DIR"
28
+
29
+ # 認証ファイルが存在しない場合は空のJSONを作成
30
+ CREDENTIALS_FILE="$HOME/.claude-sandbox-credentials.json"
31
+ if [ ! -f "$CREDENTIALS_FILE" ]; then
32
+ echo '{}' > "$CREDENTIALS_FILE"
33
+ fi
34
+
35
+ # .claude-sandbox.jsonが存在しない場合は空のJSONを作成
36
+ CLAUDE_JSON="$HOME/.claude-sandbox.json"
37
+ if [ ! -f "$CLAUDE_JSON" ]; then
38
+ echo '{}' > "$CLAUDE_JSON"
39
+ fi
40
+
41
+ # 終了時のcleanup関数
42
+ cleanup() {
43
+ echo ""
44
+ echo "Stopping Dev Container..."
45
+ docker compose -p "$PROJECT_NAME" down -v --remove-orphans 2>/dev/null || true
46
+ }
47
+
48
+ # 終了時に必ずcleanupを実行
49
+ trap cleanup EXIT
50
+
51
+ echo "Starting Dev Container (project: $PROJECT_NAME)..."
52
+ docker compose -p "$PROJECT_NAME" up -d
53
+
54
+ # コンテナが起動するまで待機
55
+ echo "Waiting for container to be ready..."
56
+ sleep 5
57
+
58
+ # コンテナIDを取得
59
+ CONTAINER_ID=$(docker compose -p "$PROJECT_NAME" ps -q devcontainer)
60
+
61
+ # devcontainer.jsonからコマンドを読み取って実行
62
+ POST_CREATE_CMD=$(jq -r '.postCreateCommand // empty' "$DEVCONTAINER_JSON")
63
+ POST_START_CMD=$(jq -r '.postStartCommand // empty' "$DEVCONTAINER_JSON")
64
+
65
+ # ワークディレクトリを設定
66
+ WORKSPACE_DIR="/workspaces/$PROJECT_NAME"
67
+
68
+ if [ -n "$POST_CREATE_CMD" ]; then
69
+ echo "Running postCreateCommand..."
70
+ # sandbox.sh経由の場合はGIT_ORIGIN_URLを/host-projectに設定
71
+ docker exec -w "$WORKSPACE_DIR" -e GIT_ORIGIN_URL=/host-project "$CONTAINER_ID" bash -c "$POST_CREATE_CMD"
72
+ fi
73
+
74
+ if [ -n "$POST_START_CMD" ]; then
75
+ echo "Running postStartCommand..."
76
+ docker exec -w "$WORKSPACE_DIR" "$CONTAINER_ID" bash -c "$POST_START_CMD"
77
+ fi
78
+
79
+ # ランダムな色コードを生成(31-36: 赤、緑、黄、青、マゼンタ、シアン)
80
+ COLORS=(31 32 33 34 35 36)
81
+ RANDOM_COLOR=${COLORS[$RANDOM % ${#COLORS[@]}]}
82
+
83
+ # カスタムPS1を設定してbashを起動
84
+ echo "Connecting to Dev Container..."
85
+ docker exec -it -w "$WORKSPACE_DIR" "$CONTAINER_ID" bash -c "export PS1='\[\e[${RANDOM_COLOR}m\]\h\[\e[0m\]:\w# '; exec bash" || true
86
+ ;;
87
+ *)
88
+ echo "Usage: sandbox [init|run]"
89
+ echo " init - Initialize devcontainer files"
90
+ echo " run - Start Dev Container (default)"
91
+ exit 1
92
+ ;;
93
+ esac