sandboxbox 2.1.1 → 2.2.1

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,272 +1,159 @@
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
17
+ - Auto-triggers on first use if Podman not found
18
+ - Verifies binary existence post-download
19
+ - Auto-initializes Podman machine on Windows after download
18
20
 
19
21
  ### Container Images
20
- - **sandboxbox-auth**: Full development environment with Claude Code
21
- - **sandboxbox-local**: Local repository workspace (symlink approach)
22
+ - **sandboxbox:latest**: Full development environment
23
+ - **sandboxbox-local:latest**: Claude Code with local repository
22
24
 
23
- ## Windows Compatibility Fixes
25
+ ## Windows Compatibility
24
26
 
25
- ### Critical PowerShell ZIP Extraction
27
+ ### Shell Execution Pattern
26
28
  ```javascript
27
- // scripts/download-podman.js:81
28
- execSync(`powershell -Command "${psCommand}"`, {
29
+ execSync(command, {
29
30
  stdio: 'pipe',
30
- cwd: __dirname,
31
- shell: true // REQUIRED for PowerShell commands
31
+ shell: process.platform === 'win32'
32
32
  });
33
33
  ```
34
34
 
35
- ### Shell Execution Pattern
36
- All `execSync()` calls must include:
35
+ ### PowerShell ZIP Extraction
37
36
  ```javascript
38
- {
37
+ execSync(`powershell -Command "${psCommand}"`, {
39
38
  stdio: 'pipe',
40
- shell: process.platform === 'win32' // Windows compatibility
41
- }
39
+ shell: true // REQUIRED
40
+ });
42
41
  ```
43
42
 
44
- ### Windows Command Interpretation
45
- - **Avoid Unix-specific syntax**: `|| true` doesn't work on Windows
46
- - **Use platform-specific error handling**:
47
- ```javascript
48
- if (process.platform === 'win32') {
49
- try {
50
- execSync(`git remote remove origin`, { stdio: 'pipe', shell: true });
51
- } catch (e) {
52
- // Ignore if origin doesn't exist
53
- }
54
- } else {
55
- execSync(`git remote remove origin 2>/dev/null || true`, { stdio: 'pipe', shell: true });
56
- }
57
- ```
43
+ ### Command Interpretation
44
+ - Avoid Unix syntax: `|| true` fails on Windows
45
+ - Use platform-specific error handling:
46
+ ```javascript
47
+ if (process.platform === 'win32') {
48
+ try {
49
+ execSync(`git remote remove origin`, { stdio: 'pipe', shell: true });
50
+ } catch (e) { /* ignore */ }
51
+ } else {
52
+ execSync(`git remote remove origin 2>/dev/null || true`, { stdio: 'pipe', shell: true });
53
+ }
54
+ ```
58
55
 
59
- ### Auto Podman Machine Management
56
+ ### Auto Podman Machine Start
60
57
  ```javascript
61
- // cli.js checkPodman() function
62
58
  if (process.platform === 'win32' && isBundled) {
63
59
  try {
64
- execSync(`"${podmanPath}" info`, { ...execOptions, stdio: 'pipe' });
60
+ execSync(`"${podmanPath}" info`, { stdio: 'pipe' });
65
61
  } catch (infoError) {
66
62
  if (infoError.message.includes('Cannot connect to Podman')) {
67
- // Auto-start existing machine
68
63
  execSync(`"${podmanPath}" machine start`, { stdio: 'inherit' });
69
64
  }
70
65
  }
71
66
  }
72
67
  ```
73
68
 
74
- ## Claude Code Integration
75
-
76
- ### Authentication Transfer
77
- Mount complete Claude session data:
78
- ```bash
79
- -v "$HOME/.claude:/root/.claude"
80
- ```
81
-
82
- ### Environment Variables
83
- Key variables to transfer:
84
- ```bash
85
- ANTHROPIC_AUTH_TOKEN
86
- CLAUDECODE=1
87
- ANTHROPIC_BASE_URL
88
- ```
89
-
90
- ### Git Identity Transfer
91
- ```bash
92
- -v "$HOME/.gitconfig:/root/.gitconfig:ro"
93
- -v "$HOME/.ssh:/root/.ssh:ro"
94
- ```
95
-
96
- ## Local Repository Workflow
97
-
98
- ### Architecture
99
- - Container mounts local repo as `/project:rw`
100
- - Creates symlink `/workspace/project` → `/project`
101
- - Works directly with local repository (no cloning needed)
102
- - Changes persist to host folder automatically
103
-
104
- ### Container Command
105
- ```bash
106
- podman run --rm \
107
- -v "/path/to/repo:/project:rw" \
108
- -v "$HOME/.claude:/root/.claude" \
109
- -v "$HOME/.gitconfig:/root/.gitconfig:ro" \
110
- -v "$HOME/.ssh:/root/.ssh" \
111
- -e "ANTHROPIC_AUTH_TOKEN=..." \
112
- -e "CLAUDECODE=1" \
113
- sandboxbox-local:latest
114
- ```
115
-
116
- ### Dockerfile.local-workspace
117
- ```dockerfile
118
- # Creates symlink to mounted repository
119
- ln -sf "$REPO_PATH" "$WORKSPACE_DIR"
120
- cd "$WORKSPACE_DIR"
121
- exec claude # Changes save directly to local repo
122
- ```
123
-
124
- ## Complete Workflow Example
125
-
126
- 1. **Setup**: Build sandboxbox-local image
127
- 2. **Mount**: Local repository as `/project:rw`
128
- 3. **Auth Transfer**: Mount `.claude`, `.gitconfig`, `.ssh`
129
- 4. **Edit**: Claude Code modifies files in `/workspace/project` (symlink to `/project`)
130
- 5. **Commit**: Changes made directly to local repository
131
- 6. **Persist**: No additional push/pull needed - changes already in host folder
132
-
133
- ## Troubleshooting
134
-
135
- ### "unzip command not found"
136
- **Solution**: Use PowerShell ZIP extraction with `shell: true`
137
-
138
- ### "Cannot connect to Podman"
139
- **Solution**: Automatic machine start in checkPodman() function
140
-
141
- ### Build context issues
142
- **Solution**: Use direct Podman build, then tag for SandboxBox
143
-
144
- ### Git identity errors
145
- **Solution**: Mount `.gitconfig:ro` for user identity transfer
146
-
147
- ### Path resolution issues
148
- **Solution**: Use explicit REPO_PATH environment variable
149
-
150
- ## Command Isolation Principles
69
+ ## Isolation Architecture
151
70
 
152
- ### Unified Architecture - All Commands Use Isolation
153
- - **`run` command**: Creates isolated temporary environment - changes DO NOT affect host
154
- - **`claude` command**: Creates isolated temporary environment - changes DO NOT affect host
155
- - **`shell` command**: Creates isolated temporary environment - changes DO NOT affect host
156
-
157
- ### Isolation Workflow
158
- 1. Copy project to temporary directory (including hidden files like .git)
71
+ ### Workflow
72
+ 1. Copy project to temporary directory (including .git)
159
73
  2. Mount temporary directory as /workspace in container
160
74
  3. Run commands in isolated environment
161
75
  4. Clean up temporary directory on exit
162
- 5. Changes are persisted via git commands (commit/push) if needed
76
+ 5. Changes persist via git push to host repository
163
77
 
164
- ### Shared Isolation Utility (utils/isolation.js)
78
+ ### Pattern
165
79
  ```javascript
166
- // All commands use the same isolation pattern
167
80
  import { createIsolatedEnvironment, setupCleanupHandlers, buildContainerMounts } from './utils/isolation.js';
168
81
 
169
- // Create isolated environment
170
82
  const { tempProjectDir, cleanup } = createIsolatedEnvironment(projectDir);
171
-
172
- // Set up cleanup handlers
173
83
  setupCleanupHandlers(cleanup);
84
+ const mounts = buildContainerMounts(tempProjectDir, projectDir);
174
85
 
175
- // Build container mounts with git identity
176
- const mounts = buildContainerMounts(tempProjectDir);
177
-
178
- // Run command with isolated directory and git identity
179
86
  execSync(`podman run --rm -it ${mounts.join(' ')} -w /workspace sandboxbox:latest ${cmd}`, {
180
87
  stdio: 'inherit',
181
88
  shell: process.platform === 'win32'
182
89
  });
183
90
 
184
- // Clean up on completion
185
91
  cleanup();
186
92
  ```
187
93
 
94
+ ## Git Integration
95
+
188
96
  ### Git Identity Transfer
189
- All commands automatically mount git identity:
190
97
  ```bash
191
- -v "$HOME/.gitconfig:/root/.gitconfig:ro" # Git configuration
192
- -v "$HOME/.ssh:/root/.ssh:ro" # SSH keys for git operations
98
+ -v "$HOME/.gitconfig:/root/.gitconfig:ro"
99
+ -v "$HOME/.ssh:/root/.ssh:ro"
193
100
  ```
194
101
 
195
- ### Host Repository Git Remote Setup
196
- For git push functionality from isolated containers:
102
+ ### Git Remote Setup
197
103
  ```bash
198
104
  # Mount host repository as accessible remote
199
105
  -v "/path/to/host/repo:/host-repo:rw"
200
106
 
201
- # Configure git remote to point to mounted host repository
107
+ # Configure remote in container
202
108
  git remote add origin /host-repo
203
- git branch --set-upstream-to=origin/main main
204
109
  ```
205
110
 
206
- ### Git Push Workflow Limitations
207
- - **Windows command quoting**: Complex git commands with spaces require bash wrapper
208
- - **Interactive shells**: Use `bash -c 'command'` for complex operations
209
- - **Command chaining**: Multiple git operations work better in single container session
210
- - **Bash command execution**: Commands like `bash -c 'command'` may start interactive shell instead of executing
111
+ ### Git Safe Directory Configuration
112
+ ```bash
113
+ git config --global --add safe.directory /workspace
114
+ git config --global --add safe.directory /host-repo
115
+ git config --global --add safe.directory /host-repo/.git
116
+ ```
211
117
 
212
- ### Git Remote Setup Requirements
213
- - **Automatic remote configuration**: `git remote add origin /host-repo`
214
- - **Upstream branch setup**: Use `git push -u origin master` instead of manual upstream configuration
215
- - **Host repository mounting**: Must mount original project as `/host-repo:rw` in container
216
- - **Git identity transfer**: Requires `.gitconfig` and `.ssh` directory mounting
217
- - **Windows path normalization**: Critical for cross-platform compatibility:
218
- ```javascript
219
- // Normalize paths for cross-platform compatibility
220
- const normalizedTempDir = tempProjectDir.replace(/\\/g, '/');
221
- ```
222
- - **Git safe directory configuration**: Required for mounted repositories:
223
- ```bash
224
- git config --global --add safe.directory /workspace
225
- git config --global --add safe.directory /host-repo
226
- git config --global --add safe.directory /host-repo/.git
227
- ```
228
- - **Host repository push configuration**: Allow pushes to checked-out branch:
229
- ```bash
230
- git config receive.denyCurrentBranch ignore
231
- ```
232
- - **Git identity setup**: Container requires explicit git user configuration:
233
- ```bash
234
- git config --global user.email "user@example.com"
235
- git config --global user.name "User Name"
236
- ```
118
+ ### Git Push Configuration
119
+ ```bash
120
+ # Host repository - allow pushes to checked-out branch
121
+ git config receive.denyCurrentBranch ignore
237
122
 
238
- ### Claude Code MCP Integration
239
- The claude command includes MCP servers and settings:
123
+ # Container - set git identity
124
+ git config --global user.email "user@example.com"
125
+ git config --global user.name "User Name"
126
+ ```
127
+
128
+ ### Windows Path Normalization
129
+ ```javascript
130
+ const normalizedTempDir = tempProjectDir.replace(/\\/g, '/');
131
+ ```
132
+
133
+ ## Claude Code Integration
134
+
135
+ ### Authentication
240
136
  ```bash
241
- # MCP servers pre-installed in container
137
+ -v "$HOME/.claude:/root/.claude"
138
+ -e "ANTHROPIC_AUTH_TOKEN=..."
139
+ -e "CLAUDECODE=1"
140
+ ```
141
+
142
+ ### MCP Servers
143
+ ```dockerfile
242
144
  RUN claude mcp add glootie -- npx -y mcp-glootie@latest
243
145
  RUN claude mcp add vexify -- npx -y mcp-vexify@latest
244
146
  RUN claude mcp add playwright -- npx @playwright/mcp@latest
245
-
246
- # Settings injection
247
- -v "./claude-settings.json:/root/.claude/settings.json:ro"
248
- -v "$HOME/.claude:/root/.claude-host:ro"
249
147
  ```
250
148
 
251
- ### Container Naming and Cleanup
252
- - Use random short names: `sandboxbox-run-${Math.random().toString(36).substr(2, 9)}`
253
- - Force cleanup: `podman rm -f container-name`
254
- - Automatic cleanup handlers for all exit scenarios
255
- - Cross-platform signal handling (SIGINT, SIGTERM)
256
-
257
- ### Cross-Platform Path Handling
258
- ```javascript
259
- // Normalize Windows paths for podman cp command
260
- const normalizedProjectDir = projectDir.replace(/\\/g, '/');
261
- ```
149
+ ## Cleanup
262
150
 
263
- ## Version Management
264
- - Publish new version when fixing critical Windows issues
265
- - Clear npm cache: `npm cache clean --force`
266
- - Use specific version: `npx sandboxbox@latest`
151
+ ### Container Cleanup
152
+ - Random names: `sandboxbox-run-${Math.random().toString(36).substr(2, 9)}`
153
+ - Force cleanup: `podman rm -f container-name`
154
+ - Automatic cleanup handlers for SIGINT, SIGTERM
267
155
 
268
- ## File Cleanup Requirements
269
- - All temporary containers auto-cleanup on exit
156
+ ### File Cleanup
270
157
  - All temporary directories auto-cleanup on exit
271
158
  - Error handling for cleanup failures (ignore errors)
272
159
  - Signal handlers ensure cleanup on interrupts
package/cli.js CHANGED
@@ -2,125 +2,13 @@
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 mounts with git identity and host remote
105
- const mounts = buildContainerMounts(tempProjectDir, projectDir);
106
-
107
- // Build claude-specific container command with mounts
108
- const containerCommand = buildClaudeContainerCommand(tempProjectDir, podmanPath, command, mounts);
109
- execSync(containerCommand, {
110
- stdio: 'inherit',
111
- shell: process.platform === 'win32'
112
- });
113
-
114
- // Clean up the temporary directory
115
- cleanup();
116
-
117
- console.log(color('green', '\n✅ Claude Code session completed! (Isolated - no host changes)'));
118
- return true;
119
- } catch (error) {
120
- console.log(color('red', `\n❌ Claude Code failed: ${error.message}`));
121
- return false;
122
- }
123
- }
10
+ import { showBanner, showHelp } from './utils/ui.js';
11
+ import { buildCommand, runCommand, shellCommand, claudeCommand, versionCommand } from './utils/commands.js';
124
12
 
125
13
  async function main() {
126
14
  const args = process.argv.slice(2);
@@ -137,29 +25,7 @@ async function main() {
137
25
  switch (command) {
138
26
  case 'build':
139
27
  const dockerfilePath = commandArgs[0] || './Dockerfile';
140
-
141
- if (!existsSync(dockerfilePath)) {
142
- console.log(color('red', `❌ Dockerfile not found: ${dockerfilePath}`));
143
- process.exit(1);
144
- }
145
-
146
- console.log(color('blue', '🏗️ Building container...'));
147
- console.log(color('yellow', `Dockerfile: ${dockerfilePath}\n`));
148
-
149
- const buildPodman = checkPodman();
150
- if (!buildPodman) process.exit(1);
151
-
152
- try {
153
- execSync(`"${buildPodman}" build -f "${dockerfilePath}" -t sandboxbox:latest .`, {
154
- stdio: 'inherit',
155
- cwd: dirname(dockerfilePath),
156
- shell: process.platform === 'win32'
157
- });
158
- console.log(color('green', '\n✅ Container built successfully!'));
159
- } catch (error) {
160
- console.log(color('red', `\n❌ Build failed: ${error.message}`));
161
- process.exit(1);
162
- }
28
+ if (!buildCommand(dockerfilePath)) process.exit(1);
163
29
  break;
164
30
 
165
31
  case 'run':
@@ -168,47 +34,9 @@ async function main() {
168
34
  console.log(color('yellow', 'Usage: npx sandboxbox run <project-dir> [command]'));
169
35
  process.exit(1);
170
36
  }
171
-
172
37
  const projectDir = resolve(commandArgs[0]);
173
38
  const cmd = commandArgs[1] || 'bash';
174
-
175
- if (!existsSync(projectDir)) {
176
- console.log(color('red', `❌ Project directory not found: ${projectDir}`));
177
- process.exit(1);
178
- }
179
-
180
- console.log(color('blue', '🚀 Running project in isolated container...'));
181
- console.log(color('yellow', `Project: ${projectDir}`));
182
- console.log(color('yellow', `Command: ${cmd}\n`));
183
- console.log(color('cyan', '📦 Note: Changes will NOT affect host files (isolated environment)'));
184
-
185
- const runPodman = checkPodman();
186
- if (!runPodman) process.exit(1);
187
-
188
- try {
189
- // Create isolated environment
190
- const { tempProjectDir, cleanup } = createIsolatedEnvironment(projectDir);
191
-
192
- // Set up cleanup handlers
193
- setupCleanupHandlers(cleanup);
194
-
195
- // Build container mounts with git identity and host remote
196
- const mounts = buildContainerMounts(tempProjectDir, projectDir);
197
-
198
- // Run the command in isolated container with temporary directory and git identity
199
- execSync(`"${runPodman}" run --rm -it ${mounts.join(' ')} -w /workspace sandboxbox:latest ${cmd}`, {
200
- stdio: 'inherit',
201
- shell: process.platform === 'win32'
202
- });
203
-
204
- // Clean up the temporary directory
205
- cleanup();
206
-
207
- console.log(color('green', '\n✅ Container execution completed! (Isolated - no host changes)'));
208
- } catch (error) {
209
- console.log(color('red', `\n❌ Run failed: ${error.message}`));
210
- process.exit(1);
211
- }
39
+ if (!runCommand(projectDir, cmd)) process.exit(1);
212
40
  break;
213
41
 
214
42
  case 'shell':
@@ -217,43 +45,8 @@ async function main() {
217
45
  console.log(color('yellow', 'Usage: npx sandboxbox shell <project-dir>'));
218
46
  process.exit(1);
219
47
  }
220
-
221
48
  const shellProjectDir = resolve(commandArgs[0]);
222
-
223
- if (!existsSync(shellProjectDir)) {
224
- console.log(color('red', `❌ Project directory not found: ${shellProjectDir}`));
225
- process.exit(1);
226
- }
227
-
228
- console.log(color('blue', '🐚 Starting interactive shell in isolated container...'));
229
- console.log(color('yellow', `Project: ${shellProjectDir}\n`));
230
- console.log(color('cyan', '📦 Note: Changes will NOT affect host files (isolated environment)'));
231
-
232
- const shellPodman = checkPodman();
233
- if (!shellPodman) process.exit(1);
234
-
235
- try {
236
- // Create isolated environment
237
- const { tempProjectDir, cleanup } = createIsolatedEnvironment(shellProjectDir);
238
-
239
- // Set up cleanup handlers
240
- setupCleanupHandlers(cleanup);
241
-
242
- // Build container mounts with git identity and host remote
243
- const mounts = buildContainerMounts(tempProjectDir, shellProjectDir);
244
-
245
- // Start interactive shell in isolated container with temporary directory and git identity
246
- execSync(`"${shellPodman}" run --rm -it ${mounts.join(' ')} -w /workspace sandboxbox:latest /bin/bash`, {
247
- stdio: 'inherit',
248
- shell: process.platform === 'win32'
249
- });
250
-
251
- // Clean up the temporary directory
252
- cleanup();
253
- } catch (error) {
254
- console.log(color('red', `\n❌ Shell failed: ${error.message}`));
255
- process.exit(1);
256
- }
49
+ if (!shellCommand(shellProjectDir)) process.exit(1);
257
50
  break;
258
51
 
259
52
  case 'claude':
@@ -262,40 +55,13 @@ async function main() {
262
55
  console.log(color('yellow', 'Usage: npx sandboxbox claude <project-dir>'));
263
56
  process.exit(1);
264
57
  }
265
-
266
58
  const claudeProjectDir = resolve(commandArgs[0]);
267
- const claudeCommand = commandArgs.slice(1).join(' ') || 'claude';
268
-
269
- // Check if Claude container exists, build if needed
270
- const podmanPath = getPodmanPath();
271
- try {
272
- execSync(`"${podmanPath}" image inspect sandboxbox-local:latest`, {
273
- stdio: 'pipe',
274
- shell: process.platform === 'win32'
275
- });
276
- } catch {
277
- console.log(color('yellow', '📦 Building Claude Code container...'));
278
- if (!buildClaudeContainer()) {
279
- process.exit(1);
280
- }
281
- }
282
-
283
- if (!runClaudeWorkspace(claudeProjectDir, claudeCommand)) {
284
- process.exit(1);
285
- }
59
+ const claudeCmd = commandArgs.slice(1).join(' ') || 'claude';
60
+ if (!claudeCommand(claudeProjectDir, claudeCmd)) process.exit(1);
286
61
  break;
287
62
 
288
63
  case 'version':
289
- try {
290
- const packageJson = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8'));
291
- console.log(color('green', `SandboxBox v${packageJson.version}`));
292
- console.log(color('cyan', 'Portable containers with Claude Code integration'));
293
- if (checkPodman()) {
294
- console.log('');
295
- }
296
- } catch (error) {
297
- console.log(color('red', '❌ Could not read version'));
298
- }
64
+ if (!versionCommand()) process.exit(1);
299
65
  break;
300
66
 
301
67
  default:
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "sandboxbox",
3
- "version": "2.1.1",
3
+ "version": "2.2.1",
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",
7
7
  "bin": {
8
- "sandboxbox": "./cli.js"
8
+ "sandboxbox": "cli.js"
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node cli.js",
@@ -29,7 +29,7 @@
29
29
  "dependencies": {},
30
30
  "repository": {
31
31
  "type": "git",
32
- "url": "https://github.com/AnEntrypoint/sandboxbox.git"
32
+ "url": "git+https://github.com/AnEntrypoint/sandboxbox.git"
33
33
  },
34
34
  "homepage": "https://github.com/AnEntrypoint/sandboxbox#readme",
35
35
  "bugs": {
@@ -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
+ }
@@ -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
+ }
package/utils/podman.js CHANGED
@@ -76,28 +76,47 @@ export function checkPodman() {
76
76
 
77
77
  return podmanPath;
78
78
  } catch (error) {
79
- if (!isBundled) {
80
- console.log(' Podman not found');
81
- console.log('\n📦 Auto-downloading Podman...');
79
+ console.log('❌ Podman not found');
80
+ console.log('\n📦 Auto-downloading Podman...');
82
81
 
83
- try {
84
- const scriptPath = resolve(__dirname, '..', 'scripts', 'download-podman.js');
85
- execSync(`node "${scriptPath}"`, { stdio: 'inherit', cwd: __dirname, shell: process.platform === 'win32' });
86
-
87
- const newPodmanPath = getPodmanPath();
88
- const execOptions = {
89
- encoding: 'utf-8',
90
- stdio: 'pipe',
91
- shell: process.platform === 'win32'
92
- };
93
- const newVersion = execSync(`"${newPodmanPath}" --version`, execOptions).trim();
94
- console.log(`\n✅ ${newVersion} (auto-downloaded)`);
95
- return newPodmanPath;
96
- } catch (downloadError) {
97
- console.log(`\n❌ Auto-download failed: ${downloadError.message}`);
82
+ try {
83
+ const scriptPath = resolve(__dirname, '..', 'scripts', 'download-podman.js');
84
+ execSync(`node "${scriptPath}"`, { stdio: 'inherit', cwd: __dirname, shell: process.platform === 'win32' });
85
+
86
+ const newPodmanPath = getPodmanPath();
87
+ if (!existsSync(newPodmanPath) && newPodmanPath !== 'podman') {
88
+ throw new Error('Download completed but binary not found');
98
89
  }
99
- } else {
100
- console.log('❌ Podman not found');
90
+
91
+ const execOptions = {
92
+ encoding: 'utf-8',
93
+ stdio: 'pipe',
94
+ shell: process.platform === 'win32'
95
+ };
96
+
97
+ const newVersion = execSync(`"${newPodmanPath}" --version`, execOptions).trim();
98
+ console.log(`\n✅ ${newVersion} (auto-downloaded)`);
99
+
100
+ if (process.platform === 'win32') {
101
+ try {
102
+ execSync(`"${newPodmanPath}" info`, { ...execOptions, stdio: 'pipe' });
103
+ } catch (infoError) {
104
+ if (infoError.message.includes('Cannot connect to Podman')) {
105
+ console.log('\n🔧 Initializing Podman machine...');
106
+ try {
107
+ execSync(`"${newPodmanPath}" machine init`, { stdio: 'inherit', shell: true });
108
+ execSync(`"${newPodmanPath}" machine start`, { stdio: 'inherit', shell: true });
109
+ console.log('\n✅ Podman machine initialized and started!');
110
+ } catch (machineError) {
111
+ console.log('\n⚠️ Podman machine initialization will be done on first use');
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ return newPodmanPath;
118
+ } catch (downloadError) {
119
+ console.log(`\n❌ Auto-download failed: ${downloadError.message}`);
101
120
  }
102
121
 
103
122
  console.log('\n💡 Please install Podman manually:');
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
+ }