sandboxbox 2.1.0 ā 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +87 -145
- package/Dockerfile +2 -2
- package/claude +0 -0
- package/cli.js +10 -241
- package/package.json +1 -1
- package/scripts/download-podman.js +5 -139
- package/scripts/podman-config.js +20 -0
- package/scripts/podman-extract.js +90 -0
- package/test-file.txt +1 -0
- package/test-from-container.txt +1 -0
- package/test-git-push.sh +22 -0
- package/utils/claude-workspace.js +12 -5
- package/utils/commands.js +193 -0
- package/utils/isolation.js +71 -2
- package/utils/ui.js +31 -0
package/CLAUDE.md
CHANGED
@@ -1,214 +1,156 @@
|
|
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
|
18
17
|
|
19
18
|
### Container Images
|
20
|
-
- **sandboxbox
|
21
|
-
- **sandboxbox-local**:
|
19
|
+
- **sandboxbox:latest**: Full development environment
|
20
|
+
- **sandboxbox-local:latest**: Claude Code with local repository
|
22
21
|
|
23
|
-
## Windows Compatibility
|
22
|
+
## Windows Compatibility
|
24
23
|
|
25
|
-
###
|
24
|
+
### Shell Execution Pattern
|
26
25
|
```javascript
|
27
|
-
|
28
|
-
execSync(`powershell -Command "${psCommand}"`, {
|
26
|
+
execSync(command, {
|
29
27
|
stdio: 'pipe',
|
30
|
-
|
31
|
-
shell: true // REQUIRED for PowerShell commands
|
28
|
+
shell: process.platform === 'win32'
|
32
29
|
});
|
33
30
|
```
|
34
31
|
|
35
|
-
###
|
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:
|
36
|
+
shell: true // REQUIRED
|
37
|
+
});
|
38
|
+
```
|
39
|
+
|
40
|
+
### Command Interpretation
|
41
|
+
- Avoid Unix syntax: `|| true` fails on Windows
|
42
|
+
- Use platform-specific error handling:
|
43
|
+
```javascript
|
44
|
+
if (process.platform === 'win32') {
|
45
|
+
try {
|
46
|
+
execSync(`git remote remove origin`, { stdio: 'pipe', shell: true });
|
47
|
+
} catch (e) { /* ignore */ }
|
48
|
+
} else {
|
49
|
+
execSync(`git remote remove origin 2>/dev/null || true`, { stdio: 'pipe', shell: true });
|
41
50
|
}
|
42
51
|
```
|
43
52
|
|
44
|
-
### Auto Podman Machine
|
53
|
+
### Auto Podman Machine Start
|
45
54
|
```javascript
|
46
|
-
// cli.js checkPodman() function
|
47
55
|
if (process.platform === 'win32' && isBundled) {
|
48
56
|
try {
|
49
|
-
execSync(`"${podmanPath}" info`, {
|
57
|
+
execSync(`"${podmanPath}" info`, { stdio: 'pipe' });
|
50
58
|
} catch (infoError) {
|
51
59
|
if (infoError.message.includes('Cannot connect to Podman')) {
|
52
|
-
// Auto-start existing machine
|
53
60
|
execSync(`"${podmanPath}" machine start`, { stdio: 'inherit' });
|
54
61
|
}
|
55
62
|
}
|
56
63
|
}
|
57
64
|
```
|
58
65
|
|
59
|
-
##
|
60
|
-
|
61
|
-
### Authentication Transfer
|
62
|
-
Mount complete Claude session data:
|
63
|
-
```bash
|
64
|
-
-v "$HOME/.claude:/root/.claude"
|
65
|
-
```
|
66
|
-
|
67
|
-
### Environment Variables
|
68
|
-
Key variables to transfer:
|
69
|
-
```bash
|
70
|
-
ANTHROPIC_AUTH_TOKEN
|
71
|
-
CLAUDECODE=1
|
72
|
-
ANTHROPIC_BASE_URL
|
73
|
-
```
|
74
|
-
|
75
|
-
### Git Identity Transfer
|
76
|
-
```bash
|
77
|
-
-v "$HOME/.gitconfig:/root/.gitconfig:ro"
|
78
|
-
-v "$HOME/.ssh:/root/.ssh:ro"
|
79
|
-
```
|
80
|
-
|
81
|
-
## Local Repository Workflow
|
82
|
-
|
83
|
-
### Architecture
|
84
|
-
- Container mounts local repo as `/project:rw`
|
85
|
-
- Creates symlink `/workspace/project` ā `/project`
|
86
|
-
- Works directly with local repository (no cloning needed)
|
87
|
-
- Changes persist to host folder automatically
|
88
|
-
|
89
|
-
### Container Command
|
90
|
-
```bash
|
91
|
-
podman run --rm \
|
92
|
-
-v "/path/to/repo:/project:rw" \
|
93
|
-
-v "$HOME/.claude:/root/.claude" \
|
94
|
-
-v "$HOME/.gitconfig:/root/.gitconfig:ro" \
|
95
|
-
-v "$HOME/.ssh:/root/.ssh" \
|
96
|
-
-e "ANTHROPIC_AUTH_TOKEN=..." \
|
97
|
-
-e "CLAUDECODE=1" \
|
98
|
-
sandboxbox-local:latest
|
99
|
-
```
|
100
|
-
|
101
|
-
### Dockerfile.local-workspace
|
102
|
-
```dockerfile
|
103
|
-
# Creates symlink to mounted repository
|
104
|
-
ln -sf "$REPO_PATH" "$WORKSPACE_DIR"
|
105
|
-
cd "$WORKSPACE_DIR"
|
106
|
-
exec claude # Changes save directly to local repo
|
107
|
-
```
|
108
|
-
|
109
|
-
## Complete Workflow Example
|
110
|
-
|
111
|
-
1. **Setup**: Build sandboxbox-local image
|
112
|
-
2. **Mount**: Local repository as `/project:rw`
|
113
|
-
3. **Auth Transfer**: Mount `.claude`, `.gitconfig`, `.ssh`
|
114
|
-
4. **Edit**: Claude Code modifies files in `/workspace/project` (symlink to `/project`)
|
115
|
-
5. **Commit**: Changes made directly to local repository
|
116
|
-
6. **Persist**: No additional push/pull needed - changes already in host folder
|
117
|
-
|
118
|
-
## Troubleshooting
|
119
|
-
|
120
|
-
### "unzip command not found"
|
121
|
-
**Solution**: Use PowerShell ZIP extraction with `shell: true`
|
66
|
+
## Isolation Architecture
|
122
67
|
|
123
|
-
###
|
124
|
-
|
125
|
-
|
126
|
-
### Build context issues
|
127
|
-
**Solution**: Use direct Podman build, then tag for SandboxBox
|
128
|
-
|
129
|
-
### Git identity errors
|
130
|
-
**Solution**: Mount `.gitconfig:ro` for user identity transfer
|
131
|
-
|
132
|
-
### Path resolution issues
|
133
|
-
**Solution**: Use explicit REPO_PATH environment variable
|
134
|
-
|
135
|
-
## Command Isolation Principles
|
136
|
-
|
137
|
-
### Unified Architecture - All Commands Use Isolation
|
138
|
-
- **`run` command**: Creates isolated temporary environment - changes DO NOT affect host
|
139
|
-
- **`claude` command**: Creates isolated temporary environment - changes DO NOT affect host
|
140
|
-
- **`shell` command**: Creates isolated temporary environment - changes DO NOT affect host
|
141
|
-
|
142
|
-
### Isolation Workflow
|
143
|
-
1. Copy project to temporary directory (including hidden files like .git)
|
68
|
+
### Workflow
|
69
|
+
1. Copy project to temporary directory (including .git)
|
144
70
|
2. Mount temporary directory as /workspace in container
|
145
71
|
3. Run commands in isolated environment
|
146
72
|
4. Clean up temporary directory on exit
|
147
|
-
5. Changes
|
73
|
+
5. Changes persist via git push to host repository
|
148
74
|
|
149
|
-
###
|
75
|
+
### Pattern
|
150
76
|
```javascript
|
151
|
-
// All commands use the same isolation pattern
|
152
77
|
import { createIsolatedEnvironment, setupCleanupHandlers, buildContainerMounts } from './utils/isolation.js';
|
153
78
|
|
154
|
-
// Create isolated environment
|
155
79
|
const { tempProjectDir, cleanup } = createIsolatedEnvironment(projectDir);
|
156
|
-
|
157
|
-
// Set up cleanup handlers
|
158
80
|
setupCleanupHandlers(cleanup);
|
81
|
+
const mounts = buildContainerMounts(tempProjectDir, projectDir);
|
159
82
|
|
160
|
-
// Build container mounts with git identity
|
161
|
-
const mounts = buildContainerMounts(tempProjectDir);
|
162
|
-
|
163
|
-
// Run command with isolated directory and git identity
|
164
83
|
execSync(`podman run --rm -it ${mounts.join(' ')} -w /workspace sandboxbox:latest ${cmd}`, {
|
165
84
|
stdio: 'inherit',
|
166
85
|
shell: process.platform === 'win32'
|
167
86
|
});
|
168
87
|
|
169
|
-
// Clean up on completion
|
170
88
|
cleanup();
|
171
89
|
```
|
172
90
|
|
91
|
+
## Git Integration
|
92
|
+
|
173
93
|
### Git Identity Transfer
|
174
|
-
All commands automatically mount git identity:
|
175
94
|
```bash
|
176
|
-
-v "$HOME/.gitconfig:/root/.gitconfig:ro"
|
177
|
-
-v "$HOME/.ssh:/root/.ssh:ro"
|
95
|
+
-v "$HOME/.gitconfig:/root/.gitconfig:ro"
|
96
|
+
-v "$HOME/.ssh:/root/.ssh:ro"
|
178
97
|
```
|
179
98
|
|
180
|
-
###
|
181
|
-
The claude command includes MCP servers and settings:
|
99
|
+
### Git Remote Setup
|
182
100
|
```bash
|
183
|
-
#
|
184
|
-
|
185
|
-
RUN claude mcp add vexify -- npx -y mcp-vexify@latest
|
186
|
-
RUN claude mcp add playwright -- npx @playwright/mcp@latest
|
101
|
+
# Mount host repository as accessible remote
|
102
|
+
-v "/path/to/host/repo:/host-repo:rw"
|
187
103
|
|
188
|
-
#
|
189
|
-
|
190
|
-
-v "$HOME/.claude:/root/.claude-host:ro"
|
104
|
+
# Configure remote in container
|
105
|
+
git remote add origin /host-repo
|
191
106
|
```
|
192
107
|
|
193
|
-
###
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
108
|
+
### Git Safe Directory Configuration
|
109
|
+
```bash
|
110
|
+
git config --global --add safe.directory /workspace
|
111
|
+
git config --global --add safe.directory /host-repo
|
112
|
+
git config --global --add safe.directory /host-repo/.git
|
113
|
+
```
|
198
114
|
|
199
|
-
###
|
115
|
+
### Git Push Configuration
|
116
|
+
```bash
|
117
|
+
# Host repository - allow pushes to checked-out branch
|
118
|
+
git config receive.denyCurrentBranch ignore
|
119
|
+
|
120
|
+
# Container - set git identity
|
121
|
+
git config --global user.email "user@example.com"
|
122
|
+
git config --global user.name "User Name"
|
123
|
+
```
|
124
|
+
|
125
|
+
### Windows Path Normalization
|
200
126
|
```javascript
|
201
|
-
|
202
|
-
|
127
|
+
const normalizedTempDir = tempProjectDir.replace(/\\/g, '/');
|
128
|
+
```
|
129
|
+
|
130
|
+
## Claude Code Integration
|
131
|
+
|
132
|
+
### Authentication
|
133
|
+
```bash
|
134
|
+
-v "$HOME/.claude:/root/.claude"
|
135
|
+
-e "ANTHROPIC_AUTH_TOKEN=..."
|
136
|
+
-e "CLAUDECODE=1"
|
203
137
|
```
|
204
138
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
139
|
+
### MCP Servers
|
140
|
+
```dockerfile
|
141
|
+
RUN claude mcp add glootie -- npx -y mcp-glootie@latest
|
142
|
+
RUN claude mcp add vexify -- npx -y mcp-vexify@latest
|
143
|
+
RUN claude mcp add playwright -- npx @playwright/mcp@latest
|
144
|
+
```
|
145
|
+
|
146
|
+
## Cleanup
|
147
|
+
|
148
|
+
### Container Cleanup
|
149
|
+
- Random names: `sandboxbox-run-${Math.random().toString(36).substr(2, 9)}`
|
150
|
+
- Force cleanup: `podman rm -f container-name`
|
151
|
+
- Automatic cleanup handlers for SIGINT, SIGTERM
|
209
152
|
|
210
|
-
|
211
|
-
- All temporary containers auto-cleanup on exit
|
153
|
+
### File Cleanup
|
212
154
|
- All temporary directories auto-cleanup on exit
|
213
155
|
- Error handling for cleanup failures (ignore errors)
|
214
156
|
- Signal handlers ensure cleanup on interrupts
|
package/Dockerfile
CHANGED
@@ -81,8 +81,8 @@ RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/
|
|
81
81
|
# Install Claude
|
82
82
|
RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}
|
83
83
|
|
84
|
-
# Install playwright deps
|
85
|
-
RUN npx --yes playwright install-deps
|
84
|
+
# Install playwright deps (commented out due to build issues)
|
85
|
+
# RUN npx --yes playwright install-deps
|
86
86
|
|
87
87
|
RUN npm i -g @playwright/mcp
|
88
88
|
|
package/claude
ADDED
File without changes
|
package/cli.js
CHANGED
@@ -1,123 +1,14 @@
|
|
1
|
-
#!/usr/bin/env node
|
1
|
+
#!/usr/bin/env node
|
2
2
|
|
3
3
|
/**
|
4
4
|
* SandboxBox CLI - Portable Container Runner with Podman
|
5
|
-
*
|
6
5
|
* Cross-platform container runner using Podman with Claude Code integration
|
7
|
-
* Works on Windows, macOS, and Linux
|
8
6
|
*/
|
9
7
|
|
10
|
-
import {
|
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 command with isolated project directory
|
105
|
-
const containerCommand = buildClaudeContainerCommand(tempProjectDir, podmanPath, command);
|
106
|
-
execSync(containerCommand, {
|
107
|
-
stdio: 'inherit',
|
108
|
-
shell: process.platform === 'win32'
|
109
|
-
});
|
110
|
-
|
111
|
-
// Clean up the temporary directory
|
112
|
-
cleanup();
|
113
|
-
|
114
|
-
console.log(color('green', '\nā
Claude Code session completed! (Isolated - no host changes)'));
|
115
|
-
return true;
|
116
|
-
} catch (error) {
|
117
|
-
console.log(color('red', `\nā Claude Code failed: ${error.message}`));
|
118
|
-
return false;
|
119
|
-
}
|
120
|
-
}
|
10
|
+
import { showBanner, showHelp } from './utils/ui.js';
|
11
|
+
import { buildCommand, runCommand, shellCommand, claudeCommand, versionCommand } from './utils/commands.js';
|
121
12
|
|
122
13
|
async function main() {
|
123
14
|
const args = process.argv.slice(2);
|
@@ -134,29 +25,7 @@ async function main() {
|
|
134
25
|
switch (command) {
|
135
26
|
case 'build':
|
136
27
|
const dockerfilePath = commandArgs[0] || './Dockerfile';
|
137
|
-
|
138
|
-
if (!existsSync(dockerfilePath)) {
|
139
|
-
console.log(color('red', `ā Dockerfile not found: ${dockerfilePath}`));
|
140
|
-
process.exit(1);
|
141
|
-
}
|
142
|
-
|
143
|
-
console.log(color('blue', 'šļø Building container...'));
|
144
|
-
console.log(color('yellow', `Dockerfile: ${dockerfilePath}\n`));
|
145
|
-
|
146
|
-
const buildPodman = checkPodman();
|
147
|
-
if (!buildPodman) process.exit(1);
|
148
|
-
|
149
|
-
try {
|
150
|
-
execSync(`"${buildPodman}" build -f "${dockerfilePath}" -t sandboxbox:latest .`, {
|
151
|
-
stdio: 'inherit',
|
152
|
-
cwd: dirname(dockerfilePath),
|
153
|
-
shell: process.platform === 'win32'
|
154
|
-
});
|
155
|
-
console.log(color('green', '\nā
Container built successfully!'));
|
156
|
-
} catch (error) {
|
157
|
-
console.log(color('red', `\nā Build failed: ${error.message}`));
|
158
|
-
process.exit(1);
|
159
|
-
}
|
28
|
+
if (!buildCommand(dockerfilePath)) process.exit(1);
|
160
29
|
break;
|
161
30
|
|
162
31
|
case 'run':
|
@@ -165,47 +34,9 @@ async function main() {
|
|
165
34
|
console.log(color('yellow', 'Usage: npx sandboxbox run <project-dir> [command]'));
|
166
35
|
process.exit(1);
|
167
36
|
}
|
168
|
-
|
169
37
|
const projectDir = resolve(commandArgs[0]);
|
170
38
|
const cmd = commandArgs[1] || 'bash';
|
171
|
-
|
172
|
-
if (!existsSync(projectDir)) {
|
173
|
-
console.log(color('red', `ā Project directory not found: ${projectDir}`));
|
174
|
-
process.exit(1);
|
175
|
-
}
|
176
|
-
|
177
|
-
console.log(color('blue', 'š Running project in isolated container...'));
|
178
|
-
console.log(color('yellow', `Project: ${projectDir}`));
|
179
|
-
console.log(color('yellow', `Command: ${cmd}\n`));
|
180
|
-
console.log(color('cyan', 'š¦ Note: Changes will NOT affect host files (isolated environment)'));
|
181
|
-
|
182
|
-
const runPodman = checkPodman();
|
183
|
-
if (!runPodman) process.exit(1);
|
184
|
-
|
185
|
-
try {
|
186
|
-
// Create isolated environment
|
187
|
-
const { tempProjectDir, cleanup } = createIsolatedEnvironment(projectDir);
|
188
|
-
|
189
|
-
// Set up cleanup handlers
|
190
|
-
setupCleanupHandlers(cleanup);
|
191
|
-
|
192
|
-
// Build container mounts with git identity
|
193
|
-
const mounts = buildContainerMounts(tempProjectDir);
|
194
|
-
|
195
|
-
// Run the command in isolated container with temporary directory and git identity
|
196
|
-
execSync(`"${runPodman}" run --rm -it ${mounts.join(' ')} -w /workspace sandboxbox:latest ${cmd}`, {
|
197
|
-
stdio: 'inherit',
|
198
|
-
shell: process.platform === 'win32'
|
199
|
-
});
|
200
|
-
|
201
|
-
// Clean up the temporary directory
|
202
|
-
cleanup();
|
203
|
-
|
204
|
-
console.log(color('green', '\nā
Container execution completed! (Isolated - no host changes)'));
|
205
|
-
} catch (error) {
|
206
|
-
console.log(color('red', `\nā Run failed: ${error.message}`));
|
207
|
-
process.exit(1);
|
208
|
-
}
|
39
|
+
if (!runCommand(projectDir, cmd)) process.exit(1);
|
209
40
|
break;
|
210
41
|
|
211
42
|
case 'shell':
|
@@ -214,43 +45,8 @@ async function main() {
|
|
214
45
|
console.log(color('yellow', 'Usage: npx sandboxbox shell <project-dir>'));
|
215
46
|
process.exit(1);
|
216
47
|
}
|
217
|
-
|
218
48
|
const shellProjectDir = resolve(commandArgs[0]);
|
219
|
-
|
220
|
-
if (!existsSync(shellProjectDir)) {
|
221
|
-
console.log(color('red', `ā Project directory not found: ${shellProjectDir}`));
|
222
|
-
process.exit(1);
|
223
|
-
}
|
224
|
-
|
225
|
-
console.log(color('blue', 'š Starting interactive shell in isolated container...'));
|
226
|
-
console.log(color('yellow', `Project: ${shellProjectDir}\n`));
|
227
|
-
console.log(color('cyan', 'š¦ Note: Changes will NOT affect host files (isolated environment)'));
|
228
|
-
|
229
|
-
const shellPodman = checkPodman();
|
230
|
-
if (!shellPodman) process.exit(1);
|
231
|
-
|
232
|
-
try {
|
233
|
-
// Create isolated environment
|
234
|
-
const { tempProjectDir, cleanup } = createIsolatedEnvironment(shellProjectDir);
|
235
|
-
|
236
|
-
// Set up cleanup handlers
|
237
|
-
setupCleanupHandlers(cleanup);
|
238
|
-
|
239
|
-
// Build container mounts with git identity
|
240
|
-
const mounts = buildContainerMounts(tempProjectDir);
|
241
|
-
|
242
|
-
// Start interactive shell in isolated container with temporary directory and git identity
|
243
|
-
execSync(`"${shellPodman}" run --rm -it ${mounts.join(' ')} -w /workspace sandboxbox:latest /bin/bash`, {
|
244
|
-
stdio: 'inherit',
|
245
|
-
shell: process.platform === 'win32'
|
246
|
-
});
|
247
|
-
|
248
|
-
// Clean up the temporary directory
|
249
|
-
cleanup();
|
250
|
-
} catch (error) {
|
251
|
-
console.log(color('red', `\nā Shell failed: ${error.message}`));
|
252
|
-
process.exit(1);
|
253
|
-
}
|
49
|
+
if (!shellCommand(shellProjectDir)) process.exit(1);
|
254
50
|
break;
|
255
51
|
|
256
52
|
case 'claude':
|
@@ -259,40 +55,13 @@ async function main() {
|
|
259
55
|
console.log(color('yellow', 'Usage: npx sandboxbox claude <project-dir>'));
|
260
56
|
process.exit(1);
|
261
57
|
}
|
262
|
-
|
263
58
|
const claudeProjectDir = resolve(commandArgs[0]);
|
264
|
-
const
|
265
|
-
|
266
|
-
// Check if Claude container exists, build if needed
|
267
|
-
const podmanPath = getPodmanPath();
|
268
|
-
try {
|
269
|
-
execSync(`"${podmanPath}" image inspect sandboxbox-local:latest`, {
|
270
|
-
stdio: 'pipe',
|
271
|
-
shell: process.platform === 'win32'
|
272
|
-
});
|
273
|
-
} catch {
|
274
|
-
console.log(color('yellow', 'š¦ Building Claude Code container...'));
|
275
|
-
if (!buildClaudeContainer()) {
|
276
|
-
process.exit(1);
|
277
|
-
}
|
278
|
-
}
|
279
|
-
|
280
|
-
if (!runClaudeWorkspace(claudeProjectDir, claudeCommand)) {
|
281
|
-
process.exit(1);
|
282
|
-
}
|
59
|
+
const claudeCmd = commandArgs.slice(1).join(' ') || 'claude';
|
60
|
+
if (!claudeCommand(claudeProjectDir, claudeCmd)) process.exit(1);
|
283
61
|
break;
|
284
62
|
|
285
63
|
case 'version':
|
286
|
-
|
287
|
-
const packageJson = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8'));
|
288
|
-
console.log(color('green', `SandboxBox v${packageJson.version}`));
|
289
|
-
console.log(color('cyan', 'Portable containers with Claude Code integration'));
|
290
|
-
if (checkPodman()) {
|
291
|
-
console.log('');
|
292
|
-
}
|
293
|
-
} catch (error) {
|
294
|
-
console.log(color('red', 'ā Could not read version'));
|
295
|
-
}
|
64
|
+
if (!versionCommand()) process.exit(1);
|
296
65
|
break;
|
297
66
|
|
298
67
|
default:
|
package/package.json
CHANGED
@@ -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
|
+
}
|
package/test-file.txt
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
'test-content'
|
@@ -0,0 +1 @@
|
|
1
|
+
'test from container'
|
package/test-git-push.sh
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
set -e
|
3
|
+
|
4
|
+
echo "=== Git Remote Check ==="
|
5
|
+
git remote -v
|
6
|
+
|
7
|
+
echo "=== File Creation ==="
|
8
|
+
echo "test content from container" > test-file.txt
|
9
|
+
|
10
|
+
echo "=== Git Status ==="
|
11
|
+
git status
|
12
|
+
|
13
|
+
echo "=== Git Add ==="
|
14
|
+
git add test-file.txt
|
15
|
+
|
16
|
+
echo "=== Git Commit ==="
|
17
|
+
git commit -m "Test commit from container"
|
18
|
+
|
19
|
+
echo "=== Git Push ==="
|
20
|
+
git push -u origin master
|
21
|
+
|
22
|
+
echo "=== Done ==="
|
@@ -14,7 +14,7 @@ export function getClaudeEnvironment() {
|
|
14
14
|
return envVars;
|
15
15
|
}
|
16
16
|
|
17
|
-
export function buildClaudeContainerCommand(projectPath, podmanPath, command = 'claude') {
|
17
|
+
export function buildClaudeContainerCommand(projectPath, podmanPath, command = 'claude', customMounts = null) {
|
18
18
|
const envVars = getClaudeEnvironment();
|
19
19
|
const envArgs = Object.entries(envVars)
|
20
20
|
.map(([key, value]) => `-e ${key}="${value}"`)
|
@@ -22,8 +22,16 @@ export function buildClaudeContainerCommand(projectPath, podmanPath, command = '
|
|
22
22
|
|
23
23
|
const homeDir = process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME;
|
24
24
|
|
25
|
-
|
26
|
-
|
25
|
+
let allMounts = [];
|
26
|
+
|
27
|
+
if (customMounts) {
|
28
|
+
// Use provided custom mounts (includes git identity and host remote)
|
29
|
+
allMounts = customMounts;
|
30
|
+
} else {
|
31
|
+
// Build base mounts from isolation utility (includes git identity)
|
32
|
+
const baseMounts = buildContainerMounts(projectPath);
|
33
|
+
allMounts = baseMounts;
|
34
|
+
}
|
27
35
|
|
28
36
|
// Add Claude-specific mounts
|
29
37
|
const claudeMounts = [
|
@@ -31,8 +39,7 @@ export function buildClaudeContainerCommand(projectPath, podmanPath, command = '
|
|
31
39
|
`-v "${process.cwd()}/claude-settings.json:/root/.claude/settings.json:ro"`
|
32
40
|
];
|
33
41
|
|
34
|
-
|
35
|
-
const allMounts = [...baseMounts, ...claudeMounts];
|
42
|
+
allMounts = [...allMounts, ...claudeMounts];
|
36
43
|
|
37
44
|
return `${podmanPath} run --rm -it ${allMounts.join(' ')} ${envArgs} --env HOME=/root sandboxbox-local:latest ${command}`;
|
38
45
|
}
|
@@ -0,0 +1,193 @@
|
|
1
|
+
import { readFileSync, existsSync, writeFileSync } from 'fs';
|
2
|
+
import { execSync } from 'child_process';
|
3
|
+
import { resolve, dirname } from 'path';
|
4
|
+
import { fileURLToPath } from 'url';
|
5
|
+
import { color } from './colors.js';
|
6
|
+
import { checkPodman, getPodmanPath } from './podman.js';
|
7
|
+
import { buildClaudeContainerCommand, createClaudeDockerfile } from './claude-workspace.js';
|
8
|
+
import { createIsolatedEnvironment, setupCleanupHandlers, buildContainerMounts } from './isolation.js';
|
9
|
+
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
11
|
+
const __dirname = dirname(__filename);
|
12
|
+
|
13
|
+
export function buildCommand(dockerfilePath) {
|
14
|
+
if (!existsSync(dockerfilePath)) {
|
15
|
+
console.log(color('red', `ā Dockerfile not found: ${dockerfilePath}`));
|
16
|
+
return false;
|
17
|
+
}
|
18
|
+
|
19
|
+
console.log(color('blue', 'šļø Building container...'));
|
20
|
+
console.log(color('yellow', `Dockerfile: ${dockerfilePath}\n`));
|
21
|
+
|
22
|
+
const podmanPath = checkPodman();
|
23
|
+
if (!podmanPath) return false;
|
24
|
+
|
25
|
+
try {
|
26
|
+
execSync(`"${podmanPath}" build -f "${dockerfilePath}" -t sandboxbox:latest .`, {
|
27
|
+
stdio: 'inherit',
|
28
|
+
cwd: dirname(dockerfilePath),
|
29
|
+
shell: process.platform === 'win32'
|
30
|
+
});
|
31
|
+
console.log(color('green', '\nā
Container built successfully!'));
|
32
|
+
return true;
|
33
|
+
} catch (error) {
|
34
|
+
console.log(color('red', `\nā Build failed: ${error.message}`));
|
35
|
+
return false;
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
export function runCommand(projectDir, cmd = 'bash') {
|
40
|
+
if (!existsSync(projectDir)) {
|
41
|
+
console.log(color('red', `ā Project directory not found: ${projectDir}`));
|
42
|
+
return false;
|
43
|
+
}
|
44
|
+
|
45
|
+
console.log(color('blue', 'š Running project in isolated container...'));
|
46
|
+
console.log(color('yellow', `Project: ${projectDir}`));
|
47
|
+
console.log(color('yellow', `Command: ${cmd}\n`));
|
48
|
+
console.log(color('cyan', 'š¦ Note: Changes will NOT affect host files (isolated environment)'));
|
49
|
+
|
50
|
+
const podmanPath = checkPodman();
|
51
|
+
if (!podmanPath) return false;
|
52
|
+
|
53
|
+
try {
|
54
|
+
const { tempProjectDir, cleanup } = createIsolatedEnvironment(projectDir);
|
55
|
+
setupCleanupHandlers(cleanup);
|
56
|
+
const mounts = buildContainerMounts(tempProjectDir, projectDir);
|
57
|
+
|
58
|
+
execSync(`"${podmanPath}" run --rm -it ${mounts.join(' ')} -w /workspace sandboxbox:latest ${cmd}`, {
|
59
|
+
stdio: 'inherit',
|
60
|
+
shell: process.platform === 'win32'
|
61
|
+
});
|
62
|
+
|
63
|
+
cleanup();
|
64
|
+
console.log(color('green', '\nā
Container execution completed! (Isolated - no host changes)'));
|
65
|
+
return true;
|
66
|
+
} catch (error) {
|
67
|
+
console.log(color('red', `\nā Run failed: ${error.message}`));
|
68
|
+
return false;
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
export function shellCommand(projectDir) {
|
73
|
+
if (!existsSync(projectDir)) {
|
74
|
+
console.log(color('red', `ā Project directory not found: ${projectDir}`));
|
75
|
+
return false;
|
76
|
+
}
|
77
|
+
|
78
|
+
console.log(color('blue', 'š Starting interactive shell in isolated container...'));
|
79
|
+
console.log(color('yellow', `Project: ${projectDir}\n`));
|
80
|
+
console.log(color('cyan', 'š¦ Note: Changes will NOT affect host files (isolated environment)'));
|
81
|
+
|
82
|
+
const podmanPath = checkPodman();
|
83
|
+
if (!podmanPath) return false;
|
84
|
+
|
85
|
+
try {
|
86
|
+
const { tempProjectDir, cleanup } = createIsolatedEnvironment(projectDir);
|
87
|
+
setupCleanupHandlers(cleanup);
|
88
|
+
const mounts = buildContainerMounts(tempProjectDir, projectDir);
|
89
|
+
|
90
|
+
execSync(`"${podmanPath}" run --rm -it ${mounts.join(' ')} -w /workspace sandboxbox:latest /bin/bash`, {
|
91
|
+
stdio: 'inherit',
|
92
|
+
shell: process.platform === 'win32'
|
93
|
+
});
|
94
|
+
|
95
|
+
cleanup();
|
96
|
+
return true;
|
97
|
+
} catch (error) {
|
98
|
+
console.log(color('red', `\nā Shell failed: ${error.message}`));
|
99
|
+
return false;
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
export function claudeCommand(projectDir, command = 'claude') {
|
104
|
+
if (!existsSync(projectDir)) {
|
105
|
+
console.log(color('red', `ā Project directory not found: ${projectDir}`));
|
106
|
+
return false;
|
107
|
+
}
|
108
|
+
|
109
|
+
if (!existsSync(resolve(projectDir, '.git'))) {
|
110
|
+
console.log(color('red', `ā Not a git repository: ${projectDir}`));
|
111
|
+
console.log(color('yellow', 'Please run this command in a git repository directory'));
|
112
|
+
return false;
|
113
|
+
}
|
114
|
+
|
115
|
+
const podmanPath = getPodmanPath();
|
116
|
+
try {
|
117
|
+
execSync(`"${podmanPath}" image inspect sandboxbox-local:latest`, {
|
118
|
+
stdio: 'pipe',
|
119
|
+
shell: process.platform === 'win32'
|
120
|
+
});
|
121
|
+
} catch {
|
122
|
+
console.log(color('yellow', 'š¦ Building Claude Code container...'));
|
123
|
+
if (!buildClaudeContainer()) {
|
124
|
+
return false;
|
125
|
+
}
|
126
|
+
}
|
127
|
+
|
128
|
+
console.log(color('blue', 'š Starting Claude Code in isolated environment...'));
|
129
|
+
console.log(color('yellow', `Project: ${projectDir}`));
|
130
|
+
console.log(color('yellow', `Command: ${command}`));
|
131
|
+
console.log(color('cyan', 'š¦ Note: Changes will be isolated and will NOT affect the original repository\n'));
|
132
|
+
|
133
|
+
const buildPodman = checkPodman();
|
134
|
+
if (!buildPodman) return false;
|
135
|
+
|
136
|
+
try {
|
137
|
+
const { tempProjectDir, cleanup } = createIsolatedEnvironment(projectDir);
|
138
|
+
setupCleanupHandlers(cleanup);
|
139
|
+
const mounts = buildContainerMounts(tempProjectDir, projectDir);
|
140
|
+
const containerCommand = buildClaudeContainerCommand(tempProjectDir, buildPodman, command, mounts);
|
141
|
+
|
142
|
+
execSync(containerCommand, {
|
143
|
+
stdio: 'inherit',
|
144
|
+
shell: process.platform === 'win32'
|
145
|
+
});
|
146
|
+
|
147
|
+
cleanup();
|
148
|
+
console.log(color('green', '\nā
Claude Code session completed! (Isolated - no host changes)'));
|
149
|
+
return true;
|
150
|
+
} catch (error) {
|
151
|
+
console.log(color('red', `\nā Claude Code failed: ${error.message}`));
|
152
|
+
return false;
|
153
|
+
}
|
154
|
+
}
|
155
|
+
|
156
|
+
export function versionCommand() {
|
157
|
+
try {
|
158
|
+
const packageJson = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'));
|
159
|
+
console.log(color('green', `SandboxBox v${packageJson.version}`));
|
160
|
+
console.log(color('cyan', 'Portable containers with Claude Code integration'));
|
161
|
+
if (checkPodman()) {
|
162
|
+
console.log('');
|
163
|
+
}
|
164
|
+
return true;
|
165
|
+
} catch (error) {
|
166
|
+
console.log(color('red', 'ā Could not read version'));
|
167
|
+
return false;
|
168
|
+
}
|
169
|
+
}
|
170
|
+
|
171
|
+
function buildClaudeContainer() {
|
172
|
+
const dockerfilePath = resolve(__dirname, '../Dockerfile.claude');
|
173
|
+
const dockerfileContent = createClaudeDockerfile();
|
174
|
+
|
175
|
+
writeFileSync(dockerfilePath, dockerfileContent);
|
176
|
+
console.log(color('blue', 'šļø Building Claude Code container...'));
|
177
|
+
|
178
|
+
const podmanPath = checkPodman();
|
179
|
+
if (!podmanPath) return false;
|
180
|
+
|
181
|
+
try {
|
182
|
+
execSync(`"${podmanPath}" build -f "${dockerfilePath}" -t sandboxbox-local:latest .`, {
|
183
|
+
stdio: 'inherit',
|
184
|
+
cwd: dirname(__dirname),
|
185
|
+
shell: process.platform === 'win32'
|
186
|
+
});
|
187
|
+
console.log(color('green', '\nā
Claude Code container built successfully!'));
|
188
|
+
return true;
|
189
|
+
} catch (error) {
|
190
|
+
console.log(color('red', `\nā Build failed: ${error.message}`));
|
191
|
+
return false;
|
192
|
+
}
|
193
|
+
}
|
package/utils/isolation.js
CHANGED
@@ -4,13 +4,17 @@ import { join } from 'path';
|
|
4
4
|
import { execSync } from 'child_process';
|
5
5
|
|
6
6
|
/**
|
7
|
-
* Builds container volume mounts with git identity
|
7
|
+
* Builds container volume mounts with git identity and host remote
|
8
8
|
* @param {string} tempProjectDir - Temporary project directory
|
9
|
+
* @param {string} originalProjectDir - Original host project directory
|
9
10
|
* @returns {Array} - Array of volume mount strings
|
10
11
|
*/
|
11
|
-
export function buildContainerMounts(tempProjectDir) {
|
12
|
+
export function buildContainerMounts(tempProjectDir, originalProjectDir) {
|
12
13
|
const mounts = [`-v "${tempProjectDir}:/workspace:rw"`];
|
13
14
|
|
15
|
+
// Add host repository as git remote
|
16
|
+
mounts.push(`-v "${originalProjectDir}:/host-repo:rw"`);
|
17
|
+
|
14
18
|
// Add git identity mounts
|
15
19
|
const homeDir = process.platform === 'win32' ? process.env.USERPROFILE : process.env.HOME;
|
16
20
|
|
@@ -72,6 +76,71 @@ export function createIsolatedEnvironment(projectDir) {
|
|
72
76
|
});
|
73
77
|
}
|
74
78
|
|
79
|
+
// Configure git remote to point to mounted host repository
|
80
|
+
try {
|
81
|
+
// Normalize paths for cross-platform compatibility
|
82
|
+
const normalizedTempDir = tempProjectDir.replace(/\\/g, '/');
|
83
|
+
const normalizedOriginalDir = projectDir.replace(/\\/g, '/');
|
84
|
+
|
85
|
+
// Configure git to allow operations in mounted directories
|
86
|
+
execSync(`git config --global --add safe.directory /workspace`, {
|
87
|
+
stdio: 'pipe',
|
88
|
+
shell: true
|
89
|
+
});
|
90
|
+
|
91
|
+
// Configure host repository to accept pushes to checked-out branch
|
92
|
+
if (process.platform === 'win32') {
|
93
|
+
try {
|
94
|
+
execSync(`cd "${normalizedOriginalDir}" && git config receive.denyCurrentBranch ignore`, {
|
95
|
+
stdio: 'pipe',
|
96
|
+
shell: true
|
97
|
+
});
|
98
|
+
} catch (e) {
|
99
|
+
// Ignore if git config fails
|
100
|
+
}
|
101
|
+
} else {
|
102
|
+
execSync(`cd "${normalizedOriginalDir}" && git config receive.denyCurrentBranch ignore`, {
|
103
|
+
stdio: 'pipe',
|
104
|
+
shell: true
|
105
|
+
});
|
106
|
+
}
|
107
|
+
|
108
|
+
// Remove any existing origin first (Windows-compatible)
|
109
|
+
if (process.platform === 'win32') {
|
110
|
+
try {
|
111
|
+
execSync(`cd "${normalizedTempDir}" && git remote remove origin`, {
|
112
|
+
stdio: 'pipe',
|
113
|
+
shell: true
|
114
|
+
});
|
115
|
+
} catch (e) {
|
116
|
+
// Ignore if origin doesn't exist
|
117
|
+
}
|
118
|
+
} else {
|
119
|
+
execSync(`cd "${normalizedTempDir}" && git remote remove origin 2>/dev/null || true`, {
|
120
|
+
stdio: 'pipe',
|
121
|
+
shell: true
|
122
|
+
});
|
123
|
+
}
|
124
|
+
|
125
|
+
// Add origin pointing to mounted host repository (accessible from container)
|
126
|
+
execSync(`cd "${normalizedTempDir}" && git remote add origin /host-repo`, {
|
127
|
+
stdio: 'pipe',
|
128
|
+
shell: true
|
129
|
+
});
|
130
|
+
|
131
|
+
// Set up upstream tracking for current branch (use push -u to set upstream)
|
132
|
+
const currentBranch = execSync(`cd "${normalizedTempDir}" && git branch --show-current`, {
|
133
|
+
encoding: 'utf8',
|
134
|
+
stdio: 'pipe'
|
135
|
+
}).trim();
|
136
|
+
|
137
|
+
// Note: Upstream will be set automatically on first push with -u flag
|
138
|
+
// No need to set up upstream manually as it may not exist yet
|
139
|
+
} catch (error) {
|
140
|
+
// Log git remote setup errors for debugging
|
141
|
+
console.error(`Git remote setup failed: ${error.message}`);
|
142
|
+
}
|
143
|
+
|
75
144
|
// Ensure cleanup on exit
|
76
145
|
const cleanup = () => {
|
77
146
|
try {
|
package/utils/ui.js
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
import { color } from './colors.js';
|
2
|
+
|
3
|
+
export function showBanner() {
|
4
|
+
console.log(color('cyan', 'š¦ SandboxBox - Portable Container Runner'));
|
5
|
+
console.log(color('cyan', 'āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
6
|
+
console.log('');
|
7
|
+
}
|
8
|
+
|
9
|
+
export function showHelp() {
|
10
|
+
console.log(color('yellow', 'Usage:'));
|
11
|
+
console.log(' npx sandboxbox <command> [options]');
|
12
|
+
console.log('');
|
13
|
+
console.log(color('yellow', 'Commands:'));
|
14
|
+
console.log(' build [dockerfile] Build container from Dockerfile');
|
15
|
+
console.log(' run <project-dir> [cmd] Run project in container');
|
16
|
+
console.log(' shell <project-dir> Start interactive shell');
|
17
|
+
console.log(' claude <project-dir> Start Claude Code with local repository');
|
18
|
+
console.log(' version Show version information');
|
19
|
+
console.log('');
|
20
|
+
console.log(color('yellow', 'Examples:'));
|
21
|
+
console.log(' npx sandboxbox build');
|
22
|
+
console.log(' npx sandboxbox claude ./my-project');
|
23
|
+
console.log(' npx sandboxbox run ./my-project "npm test"');
|
24
|
+
console.log(' npx sandboxbox shell ./my-project');
|
25
|
+
console.log('');
|
26
|
+
console.log(color('yellow', 'Requirements:'));
|
27
|
+
console.log(' - Podman (auto-downloaded if needed)');
|
28
|
+
console.log(' - Works on Windows, macOS, and Linux!');
|
29
|
+
console.log('');
|
30
|
+
console.log(color('magenta', 'š Fast startup ⢠True isolation ⢠Claude Code integration'));
|
31
|
+
}
|