sandboxbox 2.1.0 → 2.2.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/CLAUDE.md CHANGED
@@ -1,214 +1,156 @@
1
1
  # SandboxBox Technical Documentation
2
2
 
3
- ## Architecture Overview
4
- SandboxBox provides portable containerized development environments using Podman with automatic WSL machine management and Claude Code integration.
3
+ ## Architecture
4
+ Portable containerized environments using Podman with automatic WSL management and Claude Code integration.
5
5
 
6
6
  ## Core Components
7
7
 
8
8
  ### CLI (cli.js)
9
- - Main entry point with automatic Podman machine management
10
- - Commands: build, run, shell, version, help
11
- - Auto-detects and starts Podman machine when needed
12
- - Shell execution with Windows compatibility (`shell: process.platform === 'win32'`)
9
+ - Commands: build, run, shell, claude, version
10
+ - Auto-detects and starts Podman machine
11
+ - Shell execution: `shell: process.platform === 'win32'`
13
12
 
14
13
  ### Podman Downloader (scripts/download-podman.js)
15
14
  - Cross-platform binary downloads from GitHub releases
16
- - PowerShell ZIP extraction on Windows (no external dependencies)
17
- - Automatic version detection and binary path resolution
15
+ - PowerShell ZIP extraction on Windows
16
+ - Auto-detects existing installations
18
17
 
19
18
  ### Container Images
20
- - **sandboxbox-auth**: Full development environment with Claude Code
21
- - **sandboxbox-local**: Local repository workspace (symlink approach)
19
+ - **sandboxbox:latest**: Full development environment
20
+ - **sandboxbox-local:latest**: Claude Code with local repository
22
21
 
23
- ## Windows Compatibility Fixes
22
+ ## Windows Compatibility
24
23
 
25
- ### Critical PowerShell ZIP Extraction
24
+ ### Shell Execution Pattern
26
25
  ```javascript
27
- // scripts/download-podman.js:81
28
- execSync(`powershell -Command "${psCommand}"`, {
26
+ execSync(command, {
29
27
  stdio: 'pipe',
30
- cwd: __dirname,
31
- shell: true // REQUIRED for PowerShell commands
28
+ shell: process.platform === 'win32'
32
29
  });
33
30
  ```
34
31
 
35
- ### Shell Execution Pattern
36
- All `execSync()` calls must include:
32
+ ### PowerShell ZIP Extraction
37
33
  ```javascript
38
- {
34
+ execSync(`powershell -Command "${psCommand}"`, {
39
35
  stdio: 'pipe',
40
- shell: process.platform === 'win32' // Windows compatibility
36
+ shell: true // REQUIRED
37
+ });
38
+ ```
39
+
40
+ ### Command Interpretation
41
+ - Avoid Unix syntax: `|| true` fails on Windows
42
+ - Use platform-specific error handling:
43
+ ```javascript
44
+ if (process.platform === 'win32') {
45
+ try {
46
+ execSync(`git remote remove origin`, { stdio: 'pipe', shell: true });
47
+ } catch (e) { /* ignore */ }
48
+ } else {
49
+ execSync(`git remote remove origin 2>/dev/null || true`, { stdio: 'pipe', shell: true });
41
50
  }
42
51
  ```
43
52
 
44
- ### Auto Podman Machine Management
53
+ ### Auto Podman Machine Start
45
54
  ```javascript
46
- // cli.js checkPodman() function
47
55
  if (process.platform === 'win32' && isBundled) {
48
56
  try {
49
- execSync(`"${podmanPath}" info`, { ...execOptions, stdio: 'pipe' });
57
+ execSync(`"${podmanPath}" info`, { stdio: 'pipe' });
50
58
  } catch (infoError) {
51
59
  if (infoError.message.includes('Cannot connect to Podman')) {
52
- // Auto-start existing machine
53
60
  execSync(`"${podmanPath}" machine start`, { stdio: 'inherit' });
54
61
  }
55
62
  }
56
63
  }
57
64
  ```
58
65
 
59
- ## Claude Code Integration
60
-
61
- ### Authentication Transfer
62
- Mount complete Claude session data:
63
- ```bash
64
- -v "$HOME/.claude:/root/.claude"
65
- ```
66
-
67
- ### Environment Variables
68
- Key variables to transfer:
69
- ```bash
70
- ANTHROPIC_AUTH_TOKEN
71
- CLAUDECODE=1
72
- ANTHROPIC_BASE_URL
73
- ```
74
-
75
- ### Git Identity Transfer
76
- ```bash
77
- -v "$HOME/.gitconfig:/root/.gitconfig:ro"
78
- -v "$HOME/.ssh:/root/.ssh:ro"
79
- ```
80
-
81
- ## Local Repository Workflow
82
-
83
- ### Architecture
84
- - Container mounts local repo as `/project:rw`
85
- - Creates symlink `/workspace/project` → `/project`
86
- - Works directly with local repository (no cloning needed)
87
- - Changes persist to host folder automatically
88
-
89
- ### Container Command
90
- ```bash
91
- podman run --rm \
92
- -v "/path/to/repo:/project:rw" \
93
- -v "$HOME/.claude:/root/.claude" \
94
- -v "$HOME/.gitconfig:/root/.gitconfig:ro" \
95
- -v "$HOME/.ssh:/root/.ssh" \
96
- -e "ANTHROPIC_AUTH_TOKEN=..." \
97
- -e "CLAUDECODE=1" \
98
- sandboxbox-local:latest
99
- ```
100
-
101
- ### Dockerfile.local-workspace
102
- ```dockerfile
103
- # Creates symlink to mounted repository
104
- ln -sf "$REPO_PATH" "$WORKSPACE_DIR"
105
- cd "$WORKSPACE_DIR"
106
- exec claude # Changes save directly to local repo
107
- ```
108
-
109
- ## Complete Workflow Example
110
-
111
- 1. **Setup**: Build sandboxbox-local image
112
- 2. **Mount**: Local repository as `/project:rw`
113
- 3. **Auth Transfer**: Mount `.claude`, `.gitconfig`, `.ssh`
114
- 4. **Edit**: Claude Code modifies files in `/workspace/project` (symlink to `/project`)
115
- 5. **Commit**: Changes made directly to local repository
116
- 6. **Persist**: No additional push/pull needed - changes already in host folder
117
-
118
- ## Troubleshooting
119
-
120
- ### "unzip command not found"
121
- **Solution**: Use PowerShell ZIP extraction with `shell: true`
66
+ ## Isolation Architecture
122
67
 
123
- ### "Cannot connect to Podman"
124
- **Solution**: Automatic machine start in checkPodman() function
125
-
126
- ### Build context issues
127
- **Solution**: Use direct Podman build, then tag for SandboxBox
128
-
129
- ### Git identity errors
130
- **Solution**: Mount `.gitconfig:ro` for user identity transfer
131
-
132
- ### Path resolution issues
133
- **Solution**: Use explicit REPO_PATH environment variable
134
-
135
- ## Command Isolation Principles
136
-
137
- ### Unified Architecture - All Commands Use Isolation
138
- - **`run` command**: Creates isolated temporary environment - changes DO NOT affect host
139
- - **`claude` command**: Creates isolated temporary environment - changes DO NOT affect host
140
- - **`shell` command**: Creates isolated temporary environment - changes DO NOT affect host
141
-
142
- ### Isolation Workflow
143
- 1. Copy project to temporary directory (including hidden files like .git)
68
+ ### Workflow
69
+ 1. Copy project to temporary directory (including .git)
144
70
  2. Mount temporary directory as /workspace in container
145
71
  3. Run commands in isolated environment
146
72
  4. Clean up temporary directory on exit
147
- 5. Changes are persisted via git commands (commit/push) if needed
73
+ 5. Changes persist via git push to host repository
148
74
 
149
- ### Shared Isolation Utility (utils/isolation.js)
75
+ ### Pattern
150
76
  ```javascript
151
- // All commands use the same isolation pattern
152
77
  import { createIsolatedEnvironment, setupCleanupHandlers, buildContainerMounts } from './utils/isolation.js';
153
78
 
154
- // Create isolated environment
155
79
  const { tempProjectDir, cleanup } = createIsolatedEnvironment(projectDir);
156
-
157
- // Set up cleanup handlers
158
80
  setupCleanupHandlers(cleanup);
81
+ const mounts = buildContainerMounts(tempProjectDir, projectDir);
159
82
 
160
- // Build container mounts with git identity
161
- const mounts = buildContainerMounts(tempProjectDir);
162
-
163
- // Run command with isolated directory and git identity
164
83
  execSync(`podman run --rm -it ${mounts.join(' ')} -w /workspace sandboxbox:latest ${cmd}`, {
165
84
  stdio: 'inherit',
166
85
  shell: process.platform === 'win32'
167
86
  });
168
87
 
169
- // Clean up on completion
170
88
  cleanup();
171
89
  ```
172
90
 
91
+ ## Git Integration
92
+
173
93
  ### Git Identity Transfer
174
- All commands automatically mount git identity:
175
94
  ```bash
176
- -v "$HOME/.gitconfig:/root/.gitconfig:ro" # Git configuration
177
- -v "$HOME/.ssh:/root/.ssh:ro" # SSH keys for git operations
95
+ -v "$HOME/.gitconfig:/root/.gitconfig:ro"
96
+ -v "$HOME/.ssh:/root/.ssh:ro"
178
97
  ```
179
98
 
180
- ### Claude Code MCP Integration
181
- The claude command includes MCP servers and settings:
99
+ ### Git Remote Setup
182
100
  ```bash
183
- # MCP servers pre-installed in container
184
- RUN claude mcp add glootie -- npx -y mcp-glootie@latest
185
- RUN claude mcp add vexify -- npx -y mcp-vexify@latest
186
- RUN claude mcp add playwright -- npx @playwright/mcp@latest
101
+ # Mount host repository as accessible remote
102
+ -v "/path/to/host/repo:/host-repo:rw"
187
103
 
188
- # Settings injection
189
- -v "./claude-settings.json:/root/.claude/settings.json:ro"
190
- -v "$HOME/.claude:/root/.claude-host:ro"
104
+ # Configure remote in container
105
+ git remote add origin /host-repo
191
106
  ```
192
107
 
193
- ### Container Naming and Cleanup
194
- - Use random short names: `sandboxbox-run-${Math.random().toString(36).substr(2, 9)}`
195
- - Force cleanup: `podman rm -f container-name`
196
- - Automatic cleanup handlers for all exit scenarios
197
- - Cross-platform signal handling (SIGINT, SIGTERM)
108
+ ### Git Safe Directory Configuration
109
+ ```bash
110
+ git config --global --add safe.directory /workspace
111
+ git config --global --add safe.directory /host-repo
112
+ git config --global --add safe.directory /host-repo/.git
113
+ ```
198
114
 
199
- ### Cross-Platform Path Handling
115
+ ### Git Push Configuration
116
+ ```bash
117
+ # Host repository - allow pushes to checked-out branch
118
+ git config receive.denyCurrentBranch ignore
119
+
120
+ # Container - set git identity
121
+ git config --global user.email "user@example.com"
122
+ git config --global user.name "User Name"
123
+ ```
124
+
125
+ ### Windows Path Normalization
200
126
  ```javascript
201
- // Normalize Windows paths for podman cp command
202
- const normalizedProjectDir = projectDir.replace(/\\/g, '/');
127
+ const normalizedTempDir = tempProjectDir.replace(/\\/g, '/');
128
+ ```
129
+
130
+ ## Claude Code Integration
131
+
132
+ ### Authentication
133
+ ```bash
134
+ -v "$HOME/.claude:/root/.claude"
135
+ -e "ANTHROPIC_AUTH_TOKEN=..."
136
+ -e "CLAUDECODE=1"
203
137
  ```
204
138
 
205
- ## Version Management
206
- - Publish new version when fixing critical Windows issues
207
- - Clear npm cache: `npm cache clean --force`
208
- - Use specific version: `npx sandboxbox@latest`
139
+ ### MCP Servers
140
+ ```dockerfile
141
+ RUN claude mcp add glootie -- npx -y mcp-glootie@latest
142
+ RUN claude mcp add vexify -- npx -y mcp-vexify@latest
143
+ RUN claude mcp add playwright -- npx @playwright/mcp@latest
144
+ ```
145
+
146
+ ## Cleanup
147
+
148
+ ### Container Cleanup
149
+ - Random names: `sandboxbox-run-${Math.random().toString(36).substr(2, 9)}`
150
+ - Force cleanup: `podman rm -f container-name`
151
+ - Automatic cleanup handlers for SIGINT, SIGTERM
209
152
 
210
- ## File Cleanup Requirements
211
- - All temporary containers auto-cleanup on exit
153
+ ### File Cleanup
212
154
  - All temporary directories auto-cleanup on exit
213
155
  - Error handling for cleanup failures (ignore errors)
214
156
  - Signal handlers ensure cleanup on interrupts
package/Dockerfile CHANGED
@@ -81,8 +81,8 @@ RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/
81
81
  # Install Claude
82
82
  RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}
83
83
 
84
- # Install playwright deps
85
- RUN npx --yes playwright install-deps
84
+ # Install playwright deps (commented out due to build issues)
85
+ # RUN npx --yes playwright install-deps
86
86
 
87
87
  RUN npm i -g @playwright/mcp
88
88
 
package/claude ADDED
File without changes
package/cli.js CHANGED
@@ -1,123 +1,14 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * SandboxBox CLI - Portable Container Runner with Podman
5
- *
6
5
  * Cross-platform container runner using Podman with Claude Code integration
7
- * Works on Windows, macOS, and Linux
8
6
  */
9
7
 
10
- import { readFileSync, existsSync, writeFileSync } from 'fs';
11
- import { execSync } from 'child_process';
12
- import { resolve, dirname } from 'path';
13
- import { fileURLToPath } from 'url';
14
-
8
+ import { resolve } from 'path';
15
9
  import { color } from './utils/colors.js';
16
- import { checkPodman, getPodmanPath } from './utils/podman.js';
17
- import { buildClaudeContainerCommand, createClaudeDockerfile } from './utils/claude-workspace.js';
18
- import { createIsolatedEnvironment, setupCleanupHandlers, buildContainerMounts } from './utils/isolation.js';
19
-
20
- const __filename = fileURLToPath(import.meta.url);
21
- const __dirname = dirname(__filename);
22
-
23
- function showBanner() {
24
- console.log(color('cyan', 'šŸ“¦ SandboxBox - Portable Container Runner'));
25
- console.log(color('cyan', '═════════════════════════════════════════════════'));
26
- console.log('');
27
- }
28
-
29
- function showHelp() {
30
- console.log(color('yellow', 'Usage:'));
31
- console.log(' npx sandboxbox <command> [options]');
32
- console.log('');
33
- console.log(color('yellow', 'Commands:'));
34
- console.log(' build [dockerfile] Build container from Dockerfile');
35
- console.log(' run <project-dir> [cmd] Run project in container');
36
- console.log(' shell <project-dir> Start interactive shell');
37
- console.log(' claude <project-dir> Start Claude Code with local repository');
38
- console.log(' version Show version information');
39
- console.log('');
40
- console.log(color('yellow', 'Examples:'));
41
- console.log(' npx sandboxbox build');
42
- console.log(' npx sandboxbox claude ./my-project');
43
- console.log(' npx sandboxbox run ./my-project "npm test"');
44
- console.log(' npx sandboxbox shell ./my-project');
45
- console.log('');
46
- console.log(color('yellow', 'Requirements:'));
47
- console.log(' - Podman (auto-downloaded if needed)');
48
- console.log(' - Works on Windows, macOS, and Linux!');
49
- console.log('');
50
- console.log(color('magenta', 'šŸš€ Fast startup • True isolation • Claude Code integration'));
51
- }
52
-
53
- function buildClaudeContainer() {
54
- const dockerfilePath = resolve(__dirname, 'Dockerfile.claude');
55
- const dockerfileContent = createClaudeDockerfile();
56
-
57
- writeFileSync(dockerfilePath, dockerfileContent);
58
- console.log(color('blue', 'šŸ—ļø Building Claude Code container...'));
59
-
60
- const podmanPath = checkPodman();
61
- if (!podmanPath) return false;
62
-
63
- try {
64
- execSync(`"${podmanPath}" build -f "${dockerfilePath}" -t sandboxbox-local:latest .`, {
65
- stdio: 'inherit',
66
- cwd: __dirname,
67
- shell: process.platform === 'win32'
68
- });
69
- console.log(color('green', '\nāœ… Claude Code container built successfully!'));
70
- return true;
71
- } catch (error) {
72
- console.log(color('red', `\nāŒ Build failed: ${error.message}`));
73
- return false;
74
- }
75
- }
76
-
77
- function runClaudeWorkspace(projectDir, command = 'claude') {
78
- if (!existsSync(projectDir)) {
79
- console.log(color('red', `āŒ Project directory not found: ${projectDir}`));
80
- return false;
81
- }
82
-
83
- if (!existsSync(resolve(projectDir, '.git'))) {
84
- console.log(color('red', `āŒ Not a git repository: ${projectDir}`));
85
- console.log(color('yellow', 'Please run this command in a git repository directory'));
86
- return false;
87
- }
88
-
89
- console.log(color('blue', 'šŸš€ Starting Claude Code in isolated environment...'));
90
- console.log(color('yellow', `Project: ${projectDir}`));
91
- console.log(color('yellow', `Command: ${command}`));
92
- console.log(color('cyan', 'šŸ“¦ Note: Changes will be isolated and will NOT affect the original repository\n'));
93
-
94
- const podmanPath = checkPodman();
95
- if (!podmanPath) return false;
96
-
97
- try {
98
- // Create isolated environment
99
- const { tempProjectDir, cleanup } = createIsolatedEnvironment(projectDir);
100
-
101
- // Set up cleanup handlers
102
- setupCleanupHandlers(cleanup);
103
-
104
- // Build container command with isolated project directory
105
- const containerCommand = buildClaudeContainerCommand(tempProjectDir, podmanPath, command);
106
- execSync(containerCommand, {
107
- stdio: 'inherit',
108
- shell: process.platform === 'win32'
109
- });
110
-
111
- // Clean up the temporary directory
112
- cleanup();
113
-
114
- console.log(color('green', '\nāœ… Claude Code session completed! (Isolated - no host changes)'));
115
- return true;
116
- } catch (error) {
117
- console.log(color('red', `\nāŒ Claude Code failed: ${error.message}`));
118
- return false;
119
- }
120
- }
10
+ import { showBanner, showHelp } from './utils/ui.js';
11
+ import { buildCommand, runCommand, shellCommand, claudeCommand, versionCommand } from './utils/commands.js';
121
12
 
122
13
  async function main() {
123
14
  const args = process.argv.slice(2);
@@ -134,29 +25,7 @@ async function main() {
134
25
  switch (command) {
135
26
  case 'build':
136
27
  const dockerfilePath = commandArgs[0] || './Dockerfile';
137
-
138
- if (!existsSync(dockerfilePath)) {
139
- console.log(color('red', `āŒ Dockerfile not found: ${dockerfilePath}`));
140
- process.exit(1);
141
- }
142
-
143
- console.log(color('blue', 'šŸ—ļø Building container...'));
144
- console.log(color('yellow', `Dockerfile: ${dockerfilePath}\n`));
145
-
146
- const buildPodman = checkPodman();
147
- if (!buildPodman) process.exit(1);
148
-
149
- try {
150
- execSync(`"${buildPodman}" build -f "${dockerfilePath}" -t sandboxbox:latest .`, {
151
- stdio: 'inherit',
152
- cwd: dirname(dockerfilePath),
153
- shell: process.platform === 'win32'
154
- });
155
- console.log(color('green', '\nāœ… Container built successfully!'));
156
- } catch (error) {
157
- console.log(color('red', `\nāŒ Build failed: ${error.message}`));
158
- process.exit(1);
159
- }
28
+ if (!buildCommand(dockerfilePath)) process.exit(1);
160
29
  break;
161
30
 
162
31
  case 'run':
@@ -165,47 +34,9 @@ async function main() {
165
34
  console.log(color('yellow', 'Usage: npx sandboxbox run <project-dir> [command]'));
166
35
  process.exit(1);
167
36
  }
168
-
169
37
  const projectDir = resolve(commandArgs[0]);
170
38
  const cmd = commandArgs[1] || 'bash';
171
-
172
- if (!existsSync(projectDir)) {
173
- console.log(color('red', `āŒ Project directory not found: ${projectDir}`));
174
- process.exit(1);
175
- }
176
-
177
- console.log(color('blue', 'šŸš€ Running project in isolated container...'));
178
- console.log(color('yellow', `Project: ${projectDir}`));
179
- console.log(color('yellow', `Command: ${cmd}\n`));
180
- console.log(color('cyan', 'šŸ“¦ Note: Changes will NOT affect host files (isolated environment)'));
181
-
182
- const runPodman = checkPodman();
183
- if (!runPodman) process.exit(1);
184
-
185
- try {
186
- // Create isolated environment
187
- const { tempProjectDir, cleanup } = createIsolatedEnvironment(projectDir);
188
-
189
- // Set up cleanup handlers
190
- setupCleanupHandlers(cleanup);
191
-
192
- // Build container mounts with git identity
193
- const mounts = buildContainerMounts(tempProjectDir);
194
-
195
- // Run the command in isolated container with temporary directory and git identity
196
- execSync(`"${runPodman}" run --rm -it ${mounts.join(' ')} -w /workspace sandboxbox:latest ${cmd}`, {
197
- stdio: 'inherit',
198
- shell: process.platform === 'win32'
199
- });
200
-
201
- // Clean up the temporary directory
202
- cleanup();
203
-
204
- console.log(color('green', '\nāœ… Container execution completed! (Isolated - no host changes)'));
205
- } catch (error) {
206
- console.log(color('red', `\nāŒ Run failed: ${error.message}`));
207
- process.exit(1);
208
- }
39
+ if (!runCommand(projectDir, cmd)) process.exit(1);
209
40
  break;
210
41
 
211
42
  case 'shell':
@@ -214,43 +45,8 @@ async function main() {
214
45
  console.log(color('yellow', 'Usage: npx sandboxbox shell <project-dir>'));
215
46
  process.exit(1);
216
47
  }
217
-
218
48
  const shellProjectDir = resolve(commandArgs[0]);
219
-
220
- if (!existsSync(shellProjectDir)) {
221
- console.log(color('red', `āŒ Project directory not found: ${shellProjectDir}`));
222
- process.exit(1);
223
- }
224
-
225
- console.log(color('blue', '🐚 Starting interactive shell in isolated container...'));
226
- console.log(color('yellow', `Project: ${shellProjectDir}\n`));
227
- console.log(color('cyan', 'šŸ“¦ Note: Changes will NOT affect host files (isolated environment)'));
228
-
229
- const shellPodman = checkPodman();
230
- if (!shellPodman) process.exit(1);
231
-
232
- try {
233
- // Create isolated environment
234
- const { tempProjectDir, cleanup } = createIsolatedEnvironment(shellProjectDir);
235
-
236
- // Set up cleanup handlers
237
- setupCleanupHandlers(cleanup);
238
-
239
- // Build container mounts with git identity
240
- const mounts = buildContainerMounts(tempProjectDir);
241
-
242
- // Start interactive shell in isolated container with temporary directory and git identity
243
- execSync(`"${shellPodman}" run --rm -it ${mounts.join(' ')} -w /workspace sandboxbox:latest /bin/bash`, {
244
- stdio: 'inherit',
245
- shell: process.platform === 'win32'
246
- });
247
-
248
- // Clean up the temporary directory
249
- cleanup();
250
- } catch (error) {
251
- console.log(color('red', `\nāŒ Shell failed: ${error.message}`));
252
- process.exit(1);
253
- }
49
+ if (!shellCommand(shellProjectDir)) process.exit(1);
254
50
  break;
255
51
 
256
52
  case 'claude':
@@ -259,40 +55,13 @@ async function main() {
259
55
  console.log(color('yellow', 'Usage: npx sandboxbox claude <project-dir>'));
260
56
  process.exit(1);
261
57
  }
262
-
263
58
  const claudeProjectDir = resolve(commandArgs[0]);
264
- const claudeCommand = commandArgs.slice(1).join(' ') || 'claude';
265
-
266
- // Check if Claude container exists, build if needed
267
- const podmanPath = getPodmanPath();
268
- try {
269
- execSync(`"${podmanPath}" image inspect sandboxbox-local:latest`, {
270
- stdio: 'pipe',
271
- shell: process.platform === 'win32'
272
- });
273
- } catch {
274
- console.log(color('yellow', 'šŸ“¦ Building Claude Code container...'));
275
- if (!buildClaudeContainer()) {
276
- process.exit(1);
277
- }
278
- }
279
-
280
- if (!runClaudeWorkspace(claudeProjectDir, claudeCommand)) {
281
- process.exit(1);
282
- }
59
+ const claudeCmd = commandArgs.slice(1).join(' ') || 'claude';
60
+ if (!claudeCommand(claudeProjectDir, claudeCmd)) process.exit(1);
283
61
  break;
284
62
 
285
63
  case 'version':
286
- try {
287
- const packageJson = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8'));
288
- console.log(color('green', `SandboxBox v${packageJson.version}`));
289
- console.log(color('cyan', 'Portable containers with Claude Code integration'));
290
- if (checkPodman()) {
291
- console.log('');
292
- }
293
- } catch (error) {
294
- console.log(color('red', 'āŒ Could not read version'));
295
- }
64
+ if (!versionCommand()) process.exit(1);
296
65
  break;
297
66
 
298
67
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sandboxbox",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Portable container runner with Podman - Claude Code & Playwright support. Works on Windows, macOS, and Linux.",
5
5
  "type": "module",
6
6
  "main": "cli.js",
@@ -5,138 +5,16 @@
5
5
  * Similar to how sqlite/playwright auto-downloads platform-specific binaries
6
6
  */
7
7
 
8
- import { createWriteStream, existsSync, mkdirSync, chmodSync, unlinkSync, createReadStream, readdirSync, statSync } from 'fs';
9
- import { get as httpsGet } from 'https';
10
- import { get as httpGet } from 'http';
11
- import { join, dirname, sep } from 'path';
8
+ import { existsSync, mkdirSync, chmodSync, unlinkSync } from 'fs';
9
+ import { join, dirname } from 'path';
12
10
  import { fileURLToPath } from 'url';
13
- import { execSync } from 'child_process';
14
- import { createGunzip } from 'zlib';
15
- import { pipeline } from 'stream/promises';
11
+ import { PODMAN_VERSION, DOWNLOADS } from './podman-config.js';
12
+ import { download, extractZip, extractTarGz } from './podman-extract.js';
16
13
 
17
14
  const __filename = fileURLToPath(import.meta.url);
18
15
  const __dirname = dirname(__filename);
19
16
  const binDir = join(__dirname, '..', 'bin');
20
17
 
21
- // Podman remote client versions
22
- const PODMAN_VERSION = '4.9.3';
23
-
24
- // Get architecture
25
- const ARCH = process.arch === 'arm64' ? 'arm64' : 'amd64';
26
-
27
- const DOWNLOADS = {
28
- win32: {
29
- url: `https://github.com/containers/podman/releases/download/v${PODMAN_VERSION}/podman-remote-release-windows_amd64.zip`,
30
- binary: 'podman.exe',
31
- extract: 'unzip'
32
- },
33
- darwin: {
34
- url: `https://github.com/containers/podman/releases/download/v${PODMAN_VERSION}/podman-remote-release-darwin_${ARCH}.tar.gz`,
35
- binary: 'podman',
36
- extract: 'tar'
37
- },
38
- linux: {
39
- url: `https://github.com/containers/podman/releases/download/v${PODMAN_VERSION}/podman-remote-static-linux_${ARCH}.tar.gz`,
40
- binary: `podman-remote-static-linux_${ARCH}`,
41
- extract: 'tar'
42
- }
43
- };
44
-
45
- function download(url, dest) {
46
- return new Promise((resolve, reject) => {
47
- const get = url.startsWith('https') ? httpsGet : httpGet;
48
-
49
- get(url, (response) => {
50
- if (response.statusCode === 302 || response.statusCode === 301) {
51
- return download(response.headers.location, dest).then(resolve).catch(reject);
52
- }
53
-
54
- if (response.statusCode !== 200) {
55
- reject(new Error(`Download failed: ${response.statusCode}`));
56
- return;
57
- }
58
-
59
- const file = createWriteStream(dest);
60
- response.pipe(file);
61
-
62
- file.on('finish', () => {
63
- file.close();
64
- resolve();
65
- });
66
-
67
- file.on('error', reject);
68
- }).on('error', reject);
69
- });
70
- }
71
-
72
- // Simple ZIP extraction using built-in Node.js modules
73
- // Basic implementation that handles standard ZIP files without compression complications
74
- async function extractZip(zipPath, extractTo) {
75
- return new Promise((resolve, reject) => {
76
- try {
77
- // For Windows, we'll use PowerShell's built-in ZIP extraction capability
78
- // This is more reliable than expecting external tools
79
- const psCommand = `Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${zipPath.replace(/'/g, "''")}', '${extractTo.replace(/'/g, "''")}')`;
80
-
81
- execSync(`powershell -Command "${psCommand}"`, {
82
- stdio: 'pipe',
83
- cwd: __dirname,
84
- shell: true
85
- });
86
-
87
- resolve();
88
- } catch (error) {
89
- reject(error);
90
- }
91
- });
92
- }
93
-
94
- // Extract tar.gz files using Node.js built-in modules
95
- async function extractTarGz(tarPath, extractTo, stripComponents = 0) {
96
- return new Promise(async (resolve, reject) => {
97
- try {
98
- // First, decompress the .gz file
99
- const tarWithoutGz = tarPath.replace('.gz', '');
100
- const readStream = createReadStream(tarPath);
101
- const writeStream = createWriteStream(tarWithoutGz);
102
- const gunzip = createGunzip();
103
-
104
- await pipeline(readStream, gunzip, writeStream);
105
-
106
- // For tar extraction, we need to use system tar since implementing a full tar parser is complex
107
- // But we'll ensure it works across platforms by providing fallbacks
108
- try {
109
- execSync(`tar -xf "${tarWithoutGz}" -C "${extractTo}"${stripComponents ? ` --strip-components=${stripComponents}` : ''}`, {
110
- stdio: 'pipe',
111
- shell: process.platform === 'win32'
112
- });
113
- } catch (tarError) {
114
- // If system tar fails, try with specific flags for different platforms
115
- if (process.platform === 'win32') {
116
- // On Windows, try with different tar implementations
117
- try {
118
- execSync(`bsdtar -xf "${tarWithoutGz}" -C "${extractTo}"${stripComponents ? ` --strip-components=${stripComponents}` : ''}`, {
119
- stdio: 'pipe',
120
- shell: true
121
- });
122
- } catch (bsdtarError) {
123
- throw new Error(`Failed to extract tar archive. Please install tar or bsdtar: ${tarError.message}`);
124
- }
125
- } else {
126
- throw tarError;
127
- }
128
- }
129
-
130
- // Clean up the intermediate .tar file
131
- unlinkSync(tarWithoutGz);
132
-
133
- resolve();
134
- } catch (error) {
135
- reject(error);
136
- }
137
- });
138
- }
139
-
140
18
  async function main() {
141
19
  const platform = process.platform;
142
20
 
@@ -148,7 +26,6 @@ async function main() {
148
26
  return;
149
27
  }
150
28
 
151
- // Create bin directory
152
29
  if (!existsSync(binDir)) {
153
30
  mkdirSync(binDir, { recursive: true });
154
31
  }
@@ -156,7 +33,6 @@ async function main() {
156
33
  const { url, binary, extract } = DOWNLOADS[platform];
157
34
  const binaryPath = join(binDir, binary);
158
35
 
159
- // Check if already downloaded
160
36
  if (existsSync(binaryPath)) {
161
37
  console.log(`āœ… Podman already installed at ${binaryPath}`);
162
38
  return;
@@ -168,43 +44,35 @@ async function main() {
168
44
  const archivePath = join(binDir, archiveName);
169
45
 
170
46
  try {
171
- // Clean up any previous failed extractions using fs operations
172
47
  const extractedDir = join(binDir, 'podman-4.9.3');
173
48
  if (existsSync(extractedDir)) {
174
49
  const fs = await import('fs/promises');
175
50
  await fs.rm(extractedDir, { recursive: true, force: true });
176
51
  }
177
52
 
178
- // Download archive
179
53
  console.log(` Downloading from GitHub releases...`);
180
54
  await download(url, archivePath);
181
55
  console.log(`āœ… Downloaded successfully`);
182
56
 
183
- // Extract based on platform
184
57
  console.log(`šŸ“¦ Extracting...`);
185
58
  if (extract === 'tar') {
186
59
  await extractTarGz(archivePath, binDir, 1);
187
60
  } else if (extract === 'unzip') {
188
61
  await extractZip(archivePath, binDir);
189
62
 
190
- // Windows-specific: move podman.exe from nested directory to bin/
191
63
  if (platform === 'win32') {
192
64
  const extractedDir = join(binDir, `podman-4.9.3`);
193
65
  const extractedPodman = join(extractedDir, 'usr', 'bin', 'podman.exe');
194
66
  const targetPodman = join(binDir, binary);
195
67
 
196
68
  if (existsSync(extractedPodman)) {
197
- // Use fs operations instead of shell commands for cross-platform compatibility
198
69
  const fs = await import('fs/promises');
199
70
  await fs.copyFile(extractedPodman, targetPodman);
200
-
201
- // Clean up the extracted directory
202
71
  await fs.rm(extractedDir, { recursive: true, force: true });
203
72
  }
204
73
  }
205
74
  }
206
75
 
207
- // Make executable on Unix
208
76
  if (platform !== 'win32' && existsSync(binaryPath)) {
209
77
  chmodSync(binaryPath, 0o755);
210
78
  }
@@ -212,7 +80,6 @@ async function main() {
212
80
  console.log(`āœ… Podman remote installed successfully!`);
213
81
  console.log(` Binary: ${binaryPath}\n`);
214
82
 
215
- // Clean up archive
216
83
  if (existsSync(archivePath)) {
217
84
  unlinkSync(archivePath);
218
85
  }
@@ -228,10 +95,9 @@ async function main() {
228
95
  console.log(` sudo apt-get install podman # Ubuntu/Debian`);
229
96
  }
230
97
  console.log(`\n Or it will use system Podman if installed.\n`);
231
- // Don't fail the npm install
232
98
  }
233
99
  }
234
100
 
235
101
  main().catch(() => {
236
102
  // Silently fail - will use system Podman
237
- });
103
+ });
@@ -0,0 +1,20 @@
1
+ export const PODMAN_VERSION = '4.9.3';
2
+ export const ARCH = process.arch === 'arm64' ? 'arm64' : 'amd64';
3
+
4
+ export const DOWNLOADS = {
5
+ win32: {
6
+ url: `https://github.com/containers/podman/releases/download/v${PODMAN_VERSION}/podman-remote-release-windows_amd64.zip`,
7
+ binary: 'podman.exe',
8
+ extract: 'unzip'
9
+ },
10
+ darwin: {
11
+ url: `https://github.com/containers/podman/releases/download/v${PODMAN_VERSION}/podman-remote-release-darwin_${ARCH}.tar.gz`,
12
+ binary: 'podman',
13
+ extract: 'tar'
14
+ },
15
+ linux: {
16
+ url: `https://github.com/containers/podman/releases/download/v${PODMAN_VERSION}/podman-remote-static-linux_${ARCH}.tar.gz`,
17
+ binary: `podman-remote-static-linux_${ARCH}`,
18
+ extract: 'tar'
19
+ }
20
+ };
@@ -0,0 +1,90 @@
1
+ import { createWriteStream, createReadStream, unlinkSync } from 'fs';
2
+ import { execSync } from 'child_process';
3
+ import { createGunzip } from 'zlib';
4
+ import { pipeline } from 'stream/promises';
5
+
6
+ export async function extractZip(zipPath, extractTo) {
7
+ return new Promise((resolve, reject) => {
8
+ try {
9
+ const psCommand = `Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('${zipPath.replace(/'/g, "''")}', '${extractTo.replace(/'/g, "''")}')`;
10
+
11
+ execSync(`powershell -Command "${psCommand}"`, {
12
+ stdio: 'pipe',
13
+ shell: true
14
+ });
15
+
16
+ resolve();
17
+ } catch (error) {
18
+ reject(error);
19
+ }
20
+ });
21
+ }
22
+
23
+ export async function extractTarGz(tarPath, extractTo, stripComponents = 0) {
24
+ return new Promise(async (resolve, reject) => {
25
+ try {
26
+ const tarWithoutGz = tarPath.replace('.gz', '');
27
+ const readStream = createReadStream(tarPath);
28
+ const writeStream = createWriteStream(tarWithoutGz);
29
+ const gunzip = createGunzip();
30
+
31
+ await pipeline(readStream, gunzip, writeStream);
32
+
33
+ try {
34
+ execSync(`tar -xf "${tarWithoutGz}" -C "${extractTo}"${stripComponents ? ` --strip-components=${stripComponents}` : ''}`, {
35
+ stdio: 'pipe',
36
+ shell: process.platform === 'win32'
37
+ });
38
+ } catch (tarError) {
39
+ if (process.platform === 'win32') {
40
+ try {
41
+ execSync(`bsdtar -xf "${tarWithoutGz}" -C "${extractTo}"${stripComponents ? ` --strip-components=${stripComponents}` : ''}`, {
42
+ stdio: 'pipe',
43
+ shell: true
44
+ });
45
+ } catch (bsdtarError) {
46
+ throw new Error(`Failed to extract tar archive. Please install tar or bsdtar: ${tarError.message}`);
47
+ }
48
+ } else {
49
+ throw tarError;
50
+ }
51
+ }
52
+
53
+ unlinkSync(tarWithoutGz);
54
+ resolve();
55
+ } catch (error) {
56
+ reject(error);
57
+ }
58
+ });
59
+ }
60
+
61
+ export function download(url, dest) {
62
+ return new Promise(async (resolve, reject) => {
63
+ const { get: httpsGet } = await import('https');
64
+ const { get: httpGet } = await import('http');
65
+ const { createWriteStream } = await import('fs');
66
+
67
+ const get = url.startsWith('https') ? httpsGet : httpGet;
68
+
69
+ get(url, (response) => {
70
+ if (response.statusCode === 302 || response.statusCode === 301) {
71
+ return download(response.headers.location, dest).then(resolve).catch(reject);
72
+ }
73
+
74
+ if (response.statusCode !== 200) {
75
+ reject(new Error(`Download failed: ${response.statusCode}`));
76
+ return;
77
+ }
78
+
79
+ const file = createWriteStream(dest);
80
+ response.pipe(file);
81
+
82
+ file.on('finish', () => {
83
+ file.close();
84
+ resolve();
85
+ });
86
+
87
+ file.on('error', reject);
88
+ }).on('error', reject);
89
+ });
90
+ }
package/test-file.txt ADDED
@@ -0,0 +1 @@
1
+ 'test-content'
@@ -0,0 +1 @@
1
+ 'test from container'
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "=== Git Remote Check ==="
5
+ git remote -v
6
+
7
+ echo "=== File Creation ==="
8
+ echo "test content from container" > test-file.txt
9
+
10
+ echo "=== Git Status ==="
11
+ git status
12
+
13
+ echo "=== Git Add ==="
14
+ git add test-file.txt
15
+
16
+ echo "=== Git Commit ==="
17
+ git commit -m "Test commit from container"
18
+
19
+ echo "=== Git Push ==="
20
+ git push -u origin master
21
+
22
+ echo "=== Done ==="
@@ -14,7 +14,7 @@ export function getClaudeEnvironment() {
14
14
  return envVars;
15
15
  }
16
16
 
17
- export function buildClaudeContainerCommand(projectPath, podmanPath, command = 'claude') {
17
+ export function buildClaudeContainerCommand(projectPath, podmanPath, command = 'claude', customMounts = null) {
18
18
  const envVars = getClaudeEnvironment();
19
19
  const envArgs = Object.entries(envVars)
20
20
  .map(([key, value]) => `-e ${key}="${value}"`)
@@ -22,8 +22,16 @@ export function buildClaudeContainerCommand(projectPath, podmanPath, command = '
22
22
 
23
23
  const homeDir = process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME;
24
24
 
25
- // Build base mounts from isolation utility (includes git identity)
26
- const baseMounts = buildContainerMounts(projectPath);
25
+ let allMounts = [];
26
+
27
+ if (customMounts) {
28
+ // Use provided custom mounts (includes git identity and host remote)
29
+ allMounts = customMounts;
30
+ } else {
31
+ // Build base mounts from isolation utility (includes git identity)
32
+ const baseMounts = buildContainerMounts(projectPath);
33
+ allMounts = baseMounts;
34
+ }
27
35
 
28
36
  // Add Claude-specific mounts
29
37
  const claudeMounts = [
@@ -31,8 +39,7 @@ export function buildClaudeContainerCommand(projectPath, podmanPath, command = '
31
39
  `-v "${process.cwd()}/claude-settings.json:/root/.claude/settings.json:ro"`
32
40
  ];
33
41
 
34
- // Combine all mounts
35
- const allMounts = [...baseMounts, ...claudeMounts];
42
+ allMounts = [...allMounts, ...claudeMounts];
36
43
 
37
44
  return `${podmanPath} run --rm -it ${allMounts.join(' ')} ${envArgs} --env HOME=/root sandboxbox-local:latest ${command}`;
38
45
  }
@@ -0,0 +1,193 @@
1
+ import { readFileSync, existsSync, writeFileSync } from 'fs';
2
+ import { execSync } from 'child_process';
3
+ import { resolve, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { color } from './colors.js';
6
+ import { checkPodman, getPodmanPath } from './podman.js';
7
+ import { buildClaudeContainerCommand, createClaudeDockerfile } from './claude-workspace.js';
8
+ import { createIsolatedEnvironment, setupCleanupHandlers, buildContainerMounts } from './isolation.js';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ export function buildCommand(dockerfilePath) {
14
+ if (!existsSync(dockerfilePath)) {
15
+ console.log(color('red', `āŒ Dockerfile not found: ${dockerfilePath}`));
16
+ return false;
17
+ }
18
+
19
+ console.log(color('blue', 'šŸ—ļø Building container...'));
20
+ console.log(color('yellow', `Dockerfile: ${dockerfilePath}\n`));
21
+
22
+ const podmanPath = checkPodman();
23
+ if (!podmanPath) return false;
24
+
25
+ try {
26
+ execSync(`"${podmanPath}" build -f "${dockerfilePath}" -t sandboxbox:latest .`, {
27
+ stdio: 'inherit',
28
+ cwd: dirname(dockerfilePath),
29
+ shell: process.platform === 'win32'
30
+ });
31
+ console.log(color('green', '\nāœ… Container built successfully!'));
32
+ return true;
33
+ } catch (error) {
34
+ console.log(color('red', `\nāŒ Build failed: ${error.message}`));
35
+ return false;
36
+ }
37
+ }
38
+
39
+ export function runCommand(projectDir, cmd = 'bash') {
40
+ if (!existsSync(projectDir)) {
41
+ console.log(color('red', `āŒ Project directory not found: ${projectDir}`));
42
+ return false;
43
+ }
44
+
45
+ console.log(color('blue', 'šŸš€ Running project in isolated container...'));
46
+ console.log(color('yellow', `Project: ${projectDir}`));
47
+ console.log(color('yellow', `Command: ${cmd}\n`));
48
+ console.log(color('cyan', 'šŸ“¦ Note: Changes will NOT affect host files (isolated environment)'));
49
+
50
+ const podmanPath = checkPodman();
51
+ if (!podmanPath) return false;
52
+
53
+ try {
54
+ const { tempProjectDir, cleanup } = createIsolatedEnvironment(projectDir);
55
+ setupCleanupHandlers(cleanup);
56
+ const mounts = buildContainerMounts(tempProjectDir, projectDir);
57
+
58
+ execSync(`"${podmanPath}" run --rm -it ${mounts.join(' ')} -w /workspace sandboxbox:latest ${cmd}`, {
59
+ stdio: 'inherit',
60
+ shell: process.platform === 'win32'
61
+ });
62
+
63
+ cleanup();
64
+ console.log(color('green', '\nāœ… Container execution completed! (Isolated - no host changes)'));
65
+ return true;
66
+ } catch (error) {
67
+ console.log(color('red', `\nāŒ Run failed: ${error.message}`));
68
+ return false;
69
+ }
70
+ }
71
+
72
+ export function shellCommand(projectDir) {
73
+ if (!existsSync(projectDir)) {
74
+ console.log(color('red', `āŒ Project directory not found: ${projectDir}`));
75
+ return false;
76
+ }
77
+
78
+ console.log(color('blue', '🐚 Starting interactive shell in isolated container...'));
79
+ console.log(color('yellow', `Project: ${projectDir}\n`));
80
+ console.log(color('cyan', 'šŸ“¦ Note: Changes will NOT affect host files (isolated environment)'));
81
+
82
+ const podmanPath = checkPodman();
83
+ if (!podmanPath) return false;
84
+
85
+ try {
86
+ const { tempProjectDir, cleanup } = createIsolatedEnvironment(projectDir);
87
+ setupCleanupHandlers(cleanup);
88
+ const mounts = buildContainerMounts(tempProjectDir, projectDir);
89
+
90
+ execSync(`"${podmanPath}" run --rm -it ${mounts.join(' ')} -w /workspace sandboxbox:latest /bin/bash`, {
91
+ stdio: 'inherit',
92
+ shell: process.platform === 'win32'
93
+ });
94
+
95
+ cleanup();
96
+ return true;
97
+ } catch (error) {
98
+ console.log(color('red', `\nāŒ Shell failed: ${error.message}`));
99
+ return false;
100
+ }
101
+ }
102
+
103
+ export function claudeCommand(projectDir, command = 'claude') {
104
+ if (!existsSync(projectDir)) {
105
+ console.log(color('red', `āŒ Project directory not found: ${projectDir}`));
106
+ return false;
107
+ }
108
+
109
+ if (!existsSync(resolve(projectDir, '.git'))) {
110
+ console.log(color('red', `āŒ Not a git repository: ${projectDir}`));
111
+ console.log(color('yellow', 'Please run this command in a git repository directory'));
112
+ return false;
113
+ }
114
+
115
+ const podmanPath = getPodmanPath();
116
+ try {
117
+ execSync(`"${podmanPath}" image inspect sandboxbox-local:latest`, {
118
+ stdio: 'pipe',
119
+ shell: process.platform === 'win32'
120
+ });
121
+ } catch {
122
+ console.log(color('yellow', 'šŸ“¦ Building Claude Code container...'));
123
+ if (!buildClaudeContainer()) {
124
+ return false;
125
+ }
126
+ }
127
+
128
+ console.log(color('blue', 'šŸš€ Starting Claude Code in isolated environment...'));
129
+ console.log(color('yellow', `Project: ${projectDir}`));
130
+ console.log(color('yellow', `Command: ${command}`));
131
+ console.log(color('cyan', 'šŸ“¦ Note: Changes will be isolated and will NOT affect the original repository\n'));
132
+
133
+ const buildPodman = checkPodman();
134
+ if (!buildPodman) return false;
135
+
136
+ try {
137
+ const { tempProjectDir, cleanup } = createIsolatedEnvironment(projectDir);
138
+ setupCleanupHandlers(cleanup);
139
+ const mounts = buildContainerMounts(tempProjectDir, projectDir);
140
+ const containerCommand = buildClaudeContainerCommand(tempProjectDir, buildPodman, command, mounts);
141
+
142
+ execSync(containerCommand, {
143
+ stdio: 'inherit',
144
+ shell: process.platform === 'win32'
145
+ });
146
+
147
+ cleanup();
148
+ console.log(color('green', '\nāœ… Claude Code session completed! (Isolated - no host changes)'));
149
+ return true;
150
+ } catch (error) {
151
+ console.log(color('red', `\nāŒ Claude Code failed: ${error.message}`));
152
+ return false;
153
+ }
154
+ }
155
+
156
+ export function versionCommand() {
157
+ try {
158
+ const packageJson = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'));
159
+ console.log(color('green', `SandboxBox v${packageJson.version}`));
160
+ console.log(color('cyan', 'Portable containers with Claude Code integration'));
161
+ if (checkPodman()) {
162
+ console.log('');
163
+ }
164
+ return true;
165
+ } catch (error) {
166
+ console.log(color('red', 'āŒ Could not read version'));
167
+ return false;
168
+ }
169
+ }
170
+
171
+ function buildClaudeContainer() {
172
+ const dockerfilePath = resolve(__dirname, '../Dockerfile.claude');
173
+ const dockerfileContent = createClaudeDockerfile();
174
+
175
+ writeFileSync(dockerfilePath, dockerfileContent);
176
+ console.log(color('blue', 'šŸ—ļø Building Claude Code container...'));
177
+
178
+ const podmanPath = checkPodman();
179
+ if (!podmanPath) return false;
180
+
181
+ try {
182
+ execSync(`"${podmanPath}" build -f "${dockerfilePath}" -t sandboxbox-local:latest .`, {
183
+ stdio: 'inherit',
184
+ cwd: dirname(__dirname),
185
+ shell: process.platform === 'win32'
186
+ });
187
+ console.log(color('green', '\nāœ… Claude Code container built successfully!'));
188
+ return true;
189
+ } catch (error) {
190
+ console.log(color('red', `\nāŒ Build failed: ${error.message}`));
191
+ return false;
192
+ }
193
+ }
@@ -4,13 +4,17 @@ import { join } from 'path';
4
4
  import { execSync } from 'child_process';
5
5
 
6
6
  /**
7
- * Builds container volume mounts with git identity
7
+ * Builds container volume mounts with git identity and host remote
8
8
  * @param {string} tempProjectDir - Temporary project directory
9
+ * @param {string} originalProjectDir - Original host project directory
9
10
  * @returns {Array} - Array of volume mount strings
10
11
  */
11
- export function buildContainerMounts(tempProjectDir) {
12
+ export function buildContainerMounts(tempProjectDir, originalProjectDir) {
12
13
  const mounts = [`-v "${tempProjectDir}:/workspace:rw"`];
13
14
 
15
+ // Add host repository as git remote
16
+ mounts.push(`-v "${originalProjectDir}:/host-repo:rw"`);
17
+
14
18
  // Add git identity mounts
15
19
  const homeDir = process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME;
16
20
 
@@ -72,6 +76,71 @@ export function createIsolatedEnvironment(projectDir) {
72
76
  });
73
77
  }
74
78
 
79
+ // Configure git remote to point to mounted host repository
80
+ try {
81
+ // Normalize paths for cross-platform compatibility
82
+ const normalizedTempDir = tempProjectDir.replace(/\\/g, '/');
83
+ const normalizedOriginalDir = projectDir.replace(/\\/g, '/');
84
+
85
+ // Configure git to allow operations in mounted directories
86
+ execSync(`git config --global --add safe.directory /workspace`, {
87
+ stdio: 'pipe',
88
+ shell: true
89
+ });
90
+
91
+ // Configure host repository to accept pushes to checked-out branch
92
+ if (process.platform === 'win32') {
93
+ try {
94
+ execSync(`cd "${normalizedOriginalDir}" && git config receive.denyCurrentBranch ignore`, {
95
+ stdio: 'pipe',
96
+ shell: true
97
+ });
98
+ } catch (e) {
99
+ // Ignore if git config fails
100
+ }
101
+ } else {
102
+ execSync(`cd "${normalizedOriginalDir}" && git config receive.denyCurrentBranch ignore`, {
103
+ stdio: 'pipe',
104
+ shell: true
105
+ });
106
+ }
107
+
108
+ // Remove any existing origin first (Windows-compatible)
109
+ if (process.platform === 'win32') {
110
+ try {
111
+ execSync(`cd "${normalizedTempDir}" && git remote remove origin`, {
112
+ stdio: 'pipe',
113
+ shell: true
114
+ });
115
+ } catch (e) {
116
+ // Ignore if origin doesn't exist
117
+ }
118
+ } else {
119
+ execSync(`cd "${normalizedTempDir}" && git remote remove origin 2>/dev/null || true`, {
120
+ stdio: 'pipe',
121
+ shell: true
122
+ });
123
+ }
124
+
125
+ // Add origin pointing to mounted host repository (accessible from container)
126
+ execSync(`cd "${normalizedTempDir}" && git remote add origin /host-repo`, {
127
+ stdio: 'pipe',
128
+ shell: true
129
+ });
130
+
131
+ // Set up upstream tracking for current branch (use push -u to set upstream)
132
+ const currentBranch = execSync(`cd "${normalizedTempDir}" && git branch --show-current`, {
133
+ encoding: 'utf8',
134
+ stdio: 'pipe'
135
+ }).trim();
136
+
137
+ // Note: Upstream will be set automatically on first push with -u flag
138
+ // No need to set up upstream manually as it may not exist yet
139
+ } catch (error) {
140
+ // Log git remote setup errors for debugging
141
+ console.error(`Git remote setup failed: ${error.message}`);
142
+ }
143
+
75
144
  // Ensure cleanup on exit
76
145
  const cleanup = () => {
77
146
  try {
package/utils/ui.js ADDED
@@ -0,0 +1,31 @@
1
+ import { color } from './colors.js';
2
+
3
+ export function showBanner() {
4
+ console.log(color('cyan', 'šŸ“¦ SandboxBox - Portable Container Runner'));
5
+ console.log(color('cyan', '═════════════════════════════════════════════════'));
6
+ console.log('');
7
+ }
8
+
9
+ export function showHelp() {
10
+ console.log(color('yellow', 'Usage:'));
11
+ console.log(' npx sandboxbox <command> [options]');
12
+ console.log('');
13
+ console.log(color('yellow', 'Commands:'));
14
+ console.log(' build [dockerfile] Build container from Dockerfile');
15
+ console.log(' run <project-dir> [cmd] Run project in container');
16
+ console.log(' shell <project-dir> Start interactive shell');
17
+ console.log(' claude <project-dir> Start Claude Code with local repository');
18
+ console.log(' version Show version information');
19
+ console.log('');
20
+ console.log(color('yellow', 'Examples:'));
21
+ console.log(' npx sandboxbox build');
22
+ console.log(' npx sandboxbox claude ./my-project');
23
+ console.log(' npx sandboxbox run ./my-project "npm test"');
24
+ console.log(' npx sandboxbox shell ./my-project');
25
+ console.log('');
26
+ console.log(color('yellow', 'Requirements:'));
27
+ console.log(' - Podman (auto-downloaded if needed)');
28
+ console.log(' - Works on Windows, macOS, and Linux!');
29
+ console.log('');
30
+ console.log(color('magenta', 'šŸš€ Fast startup • True isolation • Claude Code integration'));
31
+ }