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 +82 -195
- package/cli.js +9 -243
- package/package.json +3 -3
- package/scripts/download-podman.js +5 -139
- package/scripts/podman-config.js +20 -0
- package/scripts/podman-extract.js +90 -0
- package/utils/commands.js +193 -0
- package/utils/podman.js +39 -20
- package/utils/ui.js +31 -0
package/CLAUDE.md
CHANGED
@@ -1,272 +1,159 @@
|
|
1
1
|
# SandboxBox Technical Documentation
|
2
2
|
|
3
|
-
## Architecture
|
4
|
-
|
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
|
-
-
|
10
|
-
-
|
11
|
-
-
|
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
|
17
|
-
-
|
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
|
21
|
-
- **sandboxbox-local**:
|
22
|
+
- **sandboxbox:latest**: Full development environment
|
23
|
+
- **sandboxbox-local:latest**: Claude Code with local repository
|
22
24
|
|
23
|
-
## Windows Compatibility
|
25
|
+
## Windows Compatibility
|
24
26
|
|
25
|
-
###
|
27
|
+
### Shell Execution Pattern
|
26
28
|
```javascript
|
27
|
-
|
28
|
-
execSync(`powershell -Command "${psCommand}"`, {
|
29
|
+
execSync(command, {
|
29
30
|
stdio: 'pipe',
|
30
|
-
|
31
|
-
shell: true // REQUIRED for PowerShell commands
|
31
|
+
shell: process.platform === 'win32'
|
32
32
|
});
|
33
33
|
```
|
34
34
|
|
35
|
-
###
|
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:
|
41
|
-
}
|
39
|
+
shell: true // REQUIRED
|
40
|
+
});
|
42
41
|
```
|
43
42
|
|
44
|
-
###
|
45
|
-
-
|
46
|
-
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
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`, {
|
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
|
-
##
|
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
|
-
###
|
153
|
-
|
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
|
76
|
+
5. Changes persist via git push to host repository
|
163
77
|
|
164
|
-
###
|
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"
|
192
|
-
-v "$HOME/.ssh:/root/.ssh:ro"
|
98
|
+
-v "$HOME/.gitconfig:/root/.gitconfig:ro"
|
99
|
+
-v "$HOME/.ssh:/root/.ssh:ro"
|
193
100
|
```
|
194
101
|
|
195
|
-
###
|
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
|
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
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
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
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
-
|
239
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
264
|
-
-
|
265
|
-
-
|
266
|
-
-
|
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
|
-
|
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 {
|
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 {
|
17
|
-
import {
|
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
|
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
|
-
|
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.
|
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": "
|
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 {
|
9
|
-
import {
|
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 {
|
14
|
-
import {
|
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
|
-
|
80
|
-
|
81
|
-
console.log('\n📦 Auto-downloading Podman...');
|
79
|
+
console.log('❌ Podman not found');
|
80
|
+
console.log('\n📦 Auto-downloading Podman...');
|
82
81
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
100
|
-
|
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
|
+
}
|