sandboxbox 2.1.1 → 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,272 +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
41
- }
36
+ shell: true // REQUIRED
37
+ });
42
38
  ```
43
39
 
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
- ```
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 });
50
+ }
51
+ ```
58
52
 
59
- ### Auto Podman Machine Management
53
+ ### Auto Podman Machine Start
60
54
  ```javascript
61
- // cli.js checkPodman() function
62
55
  if (process.platform === 'win32' && isBundled) {
63
56
  try {
64
- execSync(`"${podmanPath}" info`, { ...execOptions, stdio: 'pipe' });
57
+ execSync(`"${podmanPath}" info`, { stdio: 'pipe' });
65
58
  } catch (infoError) {
66
59
  if (infoError.message.includes('Cannot connect to Podman')) {
67
- // Auto-start existing machine
68
60
  execSync(`"${podmanPath}" machine start`, { stdio: 'inherit' });
69
61
  }
70
62
  }
71
63
  }
72
64
  ```
73
65
 
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
66
+ ## Isolation Architecture
151
67
 
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)
68
+ ### Workflow
69
+ 1. Copy project to temporary directory (including .git)
159
70
  2. Mount temporary directory as /workspace in container
160
71
  3. Run commands in isolated environment
161
72
  4. Clean up temporary directory on exit
162
- 5. Changes are persisted via git commands (commit/push) if needed
73
+ 5. Changes persist via git push to host repository
163
74
 
164
- ### Shared Isolation Utility (utils/isolation.js)
75
+ ### Pattern
165
76
  ```javascript
166
- // All commands use the same isolation pattern
167
77
  import { createIsolatedEnvironment, setupCleanupHandlers, buildContainerMounts } from './utils/isolation.js';
168
78
 
169
- // Create isolated environment
170
79
  const { tempProjectDir, cleanup } = createIsolatedEnvironment(projectDir);
171
-
172
- // Set up cleanup handlers
173
80
  setupCleanupHandlers(cleanup);
81
+ const mounts = buildContainerMounts(tempProjectDir, projectDir);
174
82
 
175
- // Build container mounts with git identity
176
- const mounts = buildContainerMounts(tempProjectDir);
177
-
178
- // Run command with isolated directory and git identity
179
83
  execSync(`podman run --rm -it ${mounts.join(' ')} -w /workspace sandboxbox:latest ${cmd}`, {
180
84
  stdio: 'inherit',
181
85
  shell: process.platform === 'win32'
182
86
  });
183
87
 
184
- // Clean up on completion
185
88
  cleanup();
186
89
  ```
187
90
 
91
+ ## Git Integration
92
+
188
93
  ### Git Identity Transfer
189
- All commands automatically mount git identity:
190
94
  ```bash
191
- -v "$HOME/.gitconfig:/root/.gitconfig:ro" # Git configuration
192
- -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"
193
97
  ```
194
98
 
195
- ### Host Repository Git Remote Setup
196
- For git push functionality from isolated containers:
99
+ ### Git Remote Setup
197
100
  ```bash
198
101
  # Mount host repository as accessible remote
199
102
  -v "/path/to/host/repo:/host-repo:rw"
200
103
 
201
- # Configure git remote to point to mounted host repository
104
+ # Configure remote in container
202
105
  git remote add origin /host-repo
203
- git branch --set-upstream-to=origin/main main
204
106
  ```
205
107
 
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
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
+ ```
211
114
 
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
- ```
115
+ ### Git Push Configuration
116
+ ```bash
117
+ # Host repository - allow pushes to checked-out branch
118
+ git config receive.denyCurrentBranch ignore
237
119
 
238
- ### Claude Code MCP Integration
239
- The claude command includes MCP servers and settings:
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
126
+ ```javascript
127
+ const normalizedTempDir = tempProjectDir.replace(/\\/g, '/');
128
+ ```
129
+
130
+ ## Claude Code Integration
131
+
132
+ ### Authentication
240
133
  ```bash
241
- # MCP servers pre-installed in container
134
+ -v "$HOME/.claude:/root/.claude"
135
+ -e "ANTHROPIC_AUTH_TOKEN=..."
136
+ -e "CLAUDECODE=1"
137
+ ```
138
+
139
+ ### MCP Servers
140
+ ```dockerfile
242
141
  RUN claude mcp add glootie -- npx -y mcp-glootie@latest
243
142
  RUN claude mcp add vexify -- npx -y mcp-vexify@latest
244
143
  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
144
  ```
250
145
 
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
- ```
146
+ ## Cleanup
262
147
 
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`
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
267
152
 
268
- ## File Cleanup Requirements
269
- - All temporary containers auto-cleanup on exit
153
+ ### File Cleanup
270
154
  - All temporary directories auto-cleanup on exit
271
155
  - Error handling for cleanup failures (ignore errors)
272
156
  - 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,6 +1,6 @@
1
1
  {
2
2
  "name": "sandboxbox",
3
- "version": "2.1.1",
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
+ }
@@ -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/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
+ }