vibebox 0.0.2 → 0.0.4
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/Dockerfile +48 -8
- package/README.md +37 -27
- package/container-scripts/gitignore_global +43 -0
- package/container-scripts/prompt +49 -0
- package/container-scripts/startup.sh +27 -21
- package/container-scripts/watcher.sh +38 -29
- package/dist/agents/auth.js +10 -1
- package/dist/agents/claude/index.js +4 -5
- package/dist/agents/index.js +7 -7
- package/dist/index.js +229 -96
- package/dist/update.js +5 -3
- package/package.json +3 -3
package/Dockerfile
CHANGED
|
@@ -17,6 +17,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
17
17
|
fzf \
|
|
18
18
|
gh \
|
|
19
19
|
git \
|
|
20
|
+
gosu \
|
|
20
21
|
openssh-client \
|
|
21
22
|
gnupg2 \
|
|
22
23
|
iproute2 \
|
|
@@ -38,11 +39,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
|
38
39
|
# Create parent directory for non-standard home paths (e.g., /Users/joe on macOS)
|
|
39
40
|
RUN mkdir -p $(dirname $LOCAL_HOME) 2>/dev/null || true
|
|
40
41
|
|
|
41
|
-
# Create group and user
|
|
42
|
+
# Create group and user (handle case where UID already exists, e.g. in CI)
|
|
42
43
|
RUN groupadd -g $LOCAL_GID $LOCAL_USER 2>/dev/null || true && \
|
|
43
44
|
GROUP_NAME=$(getent group $LOCAL_GID | cut -d: -f1) && \
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
if id -u $LOCAL_UID >/dev/null 2>&1; then \
|
|
46
|
+
EXISTING_USER=$(getent passwd $LOCAL_UID | cut -d: -f1) && \
|
|
47
|
+
usermod -d $LOCAL_HOME -s /bin/zsh -l $LOCAL_USER -g $GROUP_NAME $EXISTING_USER 2>/dev/null || true && \
|
|
48
|
+
mkdir -p $LOCAL_HOME && chown $LOCAL_UID:$LOCAL_GID $LOCAL_HOME; \
|
|
49
|
+
else \
|
|
50
|
+
useradd -m -s /bin/zsh -u $LOCAL_UID -g $GROUP_NAME -d $LOCAL_HOME $LOCAL_USER; \
|
|
51
|
+
fi && \
|
|
52
|
+
touch /etc/sudoers.d/vibebox-user && chmod 440 /etc/sudoers.d/vibebox-user
|
|
46
53
|
|
|
47
54
|
USER $LOCAL_USER
|
|
48
55
|
WORKDIR $LOCAL_HOME
|
|
@@ -60,16 +67,20 @@ RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | b
|
|
|
60
67
|
RUN curl -fsSL https://claude.ai/install.sh | bash && \
|
|
61
68
|
. "$NVM_DIR/nvm.sh" && npm install -g sfw
|
|
62
69
|
|
|
70
|
+
# Add SSH known hosts for common git providers
|
|
71
|
+
RUN mkdir -p $LOCAL_HOME/.ssh && chmod 700 $LOCAL_HOME/.ssh && \
|
|
72
|
+
ssh-keyscan -t ed25519,rsa github.com gitlab.com bitbucket.org >> $LOCAL_HOME/.ssh/known_hosts 2>/dev/null && \
|
|
73
|
+
chmod 600 $LOCAL_HOME/.ssh/known_hosts
|
|
74
|
+
|
|
63
75
|
# Configure zsh
|
|
64
76
|
RUN echo 'export NVM_DIR="$HOME/.nvm"' >> $LOCAL_HOME/.zshrc && \
|
|
65
77
|
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $LOCAL_HOME/.zshrc && \
|
|
66
|
-
echo '
|
|
67
|
-
echo 'setopt PROMPT_SUBST' >> $LOCAL_HOME/.zshrc && \
|
|
78
|
+
echo 'source $HOME/.local/bin/prompt' >> $LOCAL_HOME/.zshrc && \
|
|
68
79
|
echo 'alias npm="sfw npm"' >> $LOCAL_HOME/.zshrc && \
|
|
69
80
|
echo 'alias npx="sfw npx"' >> $LOCAL_HOME/.zshrc && \
|
|
70
81
|
echo '$HOME/.local/bin/port-monitor.sh &!' >> $LOCAL_HOME/.zshrc
|
|
71
82
|
|
|
72
|
-
# Copy container scripts
|
|
83
|
+
# Copy container scripts (as user)
|
|
73
84
|
COPY --chown=$LOCAL_USER:$LOCAL_GID container-scripts/ $LOCAL_HOME/.local/bin/
|
|
74
85
|
RUN chmod +x $LOCAL_HOME/.local/bin/*.sh
|
|
75
86
|
|
|
@@ -81,6 +92,35 @@ ENV SHELL=/bin/zsh \
|
|
|
81
92
|
VISUAL=nano \
|
|
82
93
|
DEVCONTAINER=true \
|
|
83
94
|
PATH="$LOCAL_HOME/.local/bin:$LOCAL_HOME/.nvm/versions/node/v$NODE_VERSION/bin:$PATH" \
|
|
84
|
-
NODE_PATH="$LOCAL_HOME/.nvm/versions/node/v$NODE_VERSION/lib/node_modules"
|
|
95
|
+
NODE_PATH="$LOCAL_HOME/.nvm/versions/node/v$NODE_VERSION/lib/node_modules" \
|
|
96
|
+
LOCAL_USER=$LOCAL_USER \
|
|
97
|
+
LOCAL_HOME=$LOCAL_HOME
|
|
98
|
+
|
|
99
|
+
# Create entrypoint (runs as root, sets up sudo if enabled, then drops to user)
|
|
100
|
+
USER root
|
|
101
|
+
RUN cat <<'ENTRYPOINT_EOF' > /entrypoint.sh
|
|
102
|
+
#!/bin/bash
|
|
103
|
+
set -e
|
|
104
|
+
LOCAL_USER="${LOCAL_USER:-coder}"
|
|
105
|
+
LOCAL_HOME="${LOCAL_HOME:-/home/$LOCAL_USER}"
|
|
106
|
+
|
|
107
|
+
# Enable sudo if VIBEBOX_SUDO=1
|
|
108
|
+
if [[ "$VIBEBOX_SUDO" == "1" ]]; then
|
|
109
|
+
echo "$LOCAL_USER ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/vibebox-user
|
|
110
|
+
chmod 440 /etc/sudoers.d/vibebox-user
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# Fix SSH socket permissions (Docker on macOS mounts as root:root)
|
|
114
|
+
# Use 600 (owner-only) instead of 666 (world-writable) for security
|
|
115
|
+
if [[ -S /ssh-agent ]]; then
|
|
116
|
+
chown "$LOCAL_USER" /ssh-agent 2>/dev/null || true
|
|
117
|
+
chmod 600 /ssh-agent 2>/dev/null || true
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Drop to user and run startup.sh (gosu preserves env vars, unlike su -)
|
|
121
|
+
exec gosu "$LOCAL_USER" "$LOCAL_HOME/.local/bin/startup.sh" "$@"
|
|
122
|
+
ENTRYPOINT_EOF
|
|
123
|
+
RUN chmod +x /entrypoint.sh
|
|
85
124
|
|
|
86
|
-
|
|
125
|
+
ENTRYPOINT ["/entrypoint.sh"]
|
|
126
|
+
CMD ["tail", "-f", "/dev/null"]
|
package/README.md
CHANGED
|
@@ -44,9 +44,9 @@ vibebox rebuild # Rebuild image with current host config
|
|
|
44
44
|
|
|
45
45
|
# Customization (experimental)
|
|
46
46
|
vibebox customize setup global # Edit ~/.vibebox/setup.sh (runs in all sandboxes)
|
|
47
|
-
vibebox customize setup local # Edit .
|
|
47
|
+
vibebox customize setup local # Edit vibebox.setup.sh (runs in this project)
|
|
48
48
|
vibebox customize image global # Edit ~/.vibebox/Dockerfile (custom base image)
|
|
49
|
-
vibebox customize image local # Edit .
|
|
49
|
+
vibebox customize image local # Edit vibebox.Dockerfile (custom project-specific image)
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
## Temporary Workspaces
|
|
@@ -100,9 +100,18 @@ Choose update strategy:
|
|
|
100
100
|
|
|
101
101
|
Git config is automatically synced from host (user.name, user.email, aliases, common settings).
|
|
102
102
|
|
|
103
|
-
**SSH agent forwarding
|
|
103
|
+
**SSH agent forwarding** and **GitHub CLI** are **disabled by default** for security (prevents prompt injection from using your credentials). Enable in `vibebox.config.json`:
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"sshAgent": true,
|
|
108
|
+
"ghAuth": true
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
When enabled:
|
|
113
|
+
- **SSH agent:** Forwarded to container. Keys never leave the host. Works with Docker Desktop (macOS) and native Docker (Linux).
|
|
114
|
+
- **GitHub CLI:** `~/.config/gh/hosts.yml` mounted read-only, `gh auth setup-git` runs automatically.
|
|
106
115
|
|
|
107
116
|
**HTTPS hint:** If using HTTPS remotes with SSH available, you'll see:
|
|
108
117
|
```
|
|
@@ -114,42 +123,43 @@ Git config is automatically synced from host (user.name, user.email, aliases, co
|
|
|
114
123
|
|
|
115
124
|
**Setup scripts** run once when a container is first created:
|
|
116
125
|
- `~/.vibebox/setup.sh` - Global, runs in all sandboxes
|
|
117
|
-
-
|
|
126
|
+
- `vibebox.setup.sh` - Local, runs in this project sandbox only
|
|
118
127
|
|
|
119
128
|
**Custom Dockerfiles** extend the base image:
|
|
120
129
|
- `~/.vibebox/Dockerfile` - Global customizations (`FROM vibebox`)
|
|
121
|
-
-
|
|
130
|
+
- `vibebox.Dockerfile` - Project-specific (`FROM vibebox:user` or `FROM vibebox`)
|
|
122
131
|
|
|
123
|
-
|
|
132
|
+
**Config** is stored in `vibebox.config.json` at project root and `~/.vibebox/config.json` globally. Local overrides global.
|
|
124
133
|
|
|
125
|
-
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"ports": ["3000", "5173"],
|
|
137
|
+
"sudo": false,
|
|
138
|
+
"sshAgent": false,
|
|
139
|
+
"ghAuth": false
|
|
140
|
+
}
|
|
141
|
+
```
|
|
126
142
|
|
|
127
|
-
|
|
143
|
+
| Option | Default | Description |
|
|
144
|
+
|--------|---------|-------------|
|
|
145
|
+
| `ports` | `["3000", "3001", ...]` | Ports to expose from sandbox |
|
|
146
|
+
| `sudo` | `false` | Enable sudo access in sandbox |
|
|
147
|
+
| `sshAgent` | `false` | Forward SSH agent to sandbox |
|
|
148
|
+
| `ghAuth` | `false` | Mount GitHub CLI credentials |
|
|
128
149
|
|
|
129
|
-
|
|
150
|
+
All `vibebox.*` files at project root are meant to be git-tracked. The `.vibebox/` folder contains only runtime artifacts and can be gitignored.
|
|
130
151
|
|
|
131
|
-
|
|
152
|
+
Image hierarchy: `vibebox` → `vibebox:user` (global) → `vibebox:<hash>` (local)
|
|
132
153
|
|
|
133
|
-
|
|
134
|
-
- **No `--dangerously-skip-permissions`**: `docker sandbox` runs Claude with all permissions bypassed. vibebox respects your existing Claude settings and permission model, including allowing you to launch with `--dangerously-skip-permissions` if that's your thing.
|
|
135
|
-
- **Shared credentials**: `docker sandbox` doesn't share auth with your host, so you log in fresh every time you create a sandbox. vibebox syncs credentials bidirectionally with your system keychain.
|
|
136
|
-
- **Works with any Docker runtime**: `docker sandbox` requires Docker Desktop. vibebox works with Docker Engine, Colima, or any Docker-compliant runtime.
|
|
137
|
-
- **Interactive shell access**: `vibebox enter` drops you into a shell inside the container. Useful for debugging, running commands, or working alongside the agent.
|
|
138
|
-
- **Port management**: vibebox automatically exposes common dev ports with dynamic host allocation, shows you the mappings when services start, and lets you add custom ports.
|
|
139
|
-
- **Persistent containers**: Containers persist between sessions. Your installed packages, build artifacts, and environment stay intact until you explicitly remove them.
|
|
154
|
+
Run `vibebox rebuild` after editing Dockerfiles.
|
|
140
155
|
|
|
141
|
-
|
|
156
|
+
## Why vibebox?
|
|
142
157
|
|
|
143
|
-
|
|
144
|
-
- **Automatic credential sync**: Credentials are extracted from your system keychain and mounted into the container. No manual token copying.
|
|
145
|
-
- **User matching**: Container user matches your host UID/GID/home path, so file permissions just work.
|
|
146
|
-
- **Built-in port detection**: When a service starts listening, vibebox shows you the mapped URL. No guessing which host port maps where.
|
|
158
|
+
**vs `docker sandbox`**: Docker Desktop's sandbox uses microVM isolation—stronger security, but a blackbox. No credential sync (re-auth every session), no customization, runs in full yolo mode. On Linux it falls back to container isolation anyway. vibebox trades some isolation for **continuity**: credentials sync, config persists, you can customize and debug.
|
|
147
159
|
|
|
148
|
-
|
|
160
|
+
**vs raw docker**: No Dockerfiles, no mount flags, no manual credential copying. Just `vibebox agent run claude`. Container user matches your UID/GID so file permissions work. Port mappings shown automatically when services start.
|
|
149
161
|
|
|
150
|
-
|
|
151
|
-
- **Simpler model**: No `devcontainer.json`, no features, no lifecycle hooks. One command to start.
|
|
152
|
-
- **Agent-first**: Built specifically for CLI agents with credential mounting, not general-purpose dev environments.
|
|
162
|
+
**vs devcontainers**: No VS Code dependency, no `devcontainer.json`. Built for CLI agents, not general dev environments.
|
|
153
163
|
|
|
154
164
|
## License
|
|
155
165
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# custom #
|
|
2
|
+
###################
|
|
3
|
+
.vibebox
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Compiled source #
|
|
7
|
+
###################
|
|
8
|
+
*.com
|
|
9
|
+
*.class
|
|
10
|
+
*.dll
|
|
11
|
+
*.exe
|
|
12
|
+
*.o
|
|
13
|
+
*.so
|
|
14
|
+
|
|
15
|
+
# Packages #
|
|
16
|
+
############
|
|
17
|
+
# it's better to unpack these files and commit the raw source
|
|
18
|
+
# git has its own built in compression methods
|
|
19
|
+
*.7z
|
|
20
|
+
*.dmg
|
|
21
|
+
*.gz
|
|
22
|
+
*.iso
|
|
23
|
+
*.jar
|
|
24
|
+
*.rar
|
|
25
|
+
*.tar
|
|
26
|
+
*.zip
|
|
27
|
+
|
|
28
|
+
# Logs and databases #
|
|
29
|
+
######################
|
|
30
|
+
*.log
|
|
31
|
+
*.sql
|
|
32
|
+
*.sqlite
|
|
33
|
+
!**/prisma/**/*.sql
|
|
34
|
+
|
|
35
|
+
# OS generated files #
|
|
36
|
+
######################
|
|
37
|
+
.DS_Store
|
|
38
|
+
.DS_Store?
|
|
39
|
+
._*
|
|
40
|
+
.Spotlight-V100
|
|
41
|
+
.Trashes
|
|
42
|
+
ehthumbs.db
|
|
43
|
+
Thumbs.db
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
prompt_git() {
|
|
2
|
+
# check if the current directory is in a git repository
|
|
3
|
+
if [ $(git rev-parse --is-inside-work-tree &>/dev/null; print $?) = 0 ]; then
|
|
4
|
+
# check if the current directory is in .git before running git checks
|
|
5
|
+
if [ "$(git rev-parse --is-inside-git-dir 2> /dev/null)" = "false" ]; then
|
|
6
|
+
|
|
7
|
+
# ensure index is up to date
|
|
8
|
+
git update-index --really-refresh -q &>/dev/null
|
|
9
|
+
|
|
10
|
+
# check for uncommitted changes in the index
|
|
11
|
+
if ! $(git diff --quiet --ignore-submodules --cached); then
|
|
12
|
+
s="$s+";
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# check for unstaged changes
|
|
16
|
+
if ! $(git diff-files --quiet --ignore-submodules --); then
|
|
17
|
+
s="$s!";
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# check for untracked files
|
|
21
|
+
if [ -n "$(git ls-files --others --exclude-standard)" ]; then
|
|
22
|
+
s="$s?";
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# check for stashed files
|
|
26
|
+
# if $(git rev-parse --verify refs/stash &>/dev/null); then
|
|
27
|
+
# s="$s$";
|
|
28
|
+
# fi
|
|
29
|
+
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# get the short symbolic ref
|
|
33
|
+
# if HEAD isn't a symbolic ref, get the short SHA
|
|
34
|
+
# otherwise, just give up
|
|
35
|
+
branchName="$(git symbolic-ref --quiet --short HEAD 2> /dev/null || \
|
|
36
|
+
git rev-parse --short HEAD 2> /dev/null || \
|
|
37
|
+
printf "(unknown)")"
|
|
38
|
+
|
|
39
|
+
[ -n "$s" ] && s="[$s] "
|
|
40
|
+
|
|
41
|
+
printf "%s %s %s" "$1 " "$branchName" "$s"
|
|
42
|
+
else
|
|
43
|
+
return
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Set up the prompt (with git branch name)
|
|
48
|
+
setopt prompt_subst
|
|
49
|
+
PROMPT='🇻 %~ $(prompt_git '⎇')%(!.#.\$) '
|
|
@@ -3,7 +3,8 @@ set -e
|
|
|
3
3
|
|
|
4
4
|
HOME_DIR="$HOME"
|
|
5
5
|
WORKSPACE="${VIBEBOX_PROJECT_ROOT:-$(pwd)}"
|
|
6
|
-
|
|
6
|
+
# Store setup marker in workspace .vibebox/ (writable), not ~/.vibebox (read-only)
|
|
7
|
+
SETUP_MARKER="$WORKSPACE/.vibebox/.setup-done"
|
|
7
8
|
|
|
8
9
|
# ============ Git Config Sync ============
|
|
9
10
|
# Apply safe git settings from host (passed as VIBEBOX_GIT_* env vars)
|
|
@@ -17,14 +18,21 @@ setup_git_config() {
|
|
|
17
18
|
[[ -n "$VIBEBOX_GIT_PULL_REBASE" ]] && git config --global pull.rebase "$VIBEBOX_GIT_PULL_REBASE"
|
|
18
19
|
[[ -n "$VIBEBOX_GIT_HELP_AUTOCORRECT" ]] && git config --global help.autocorrect "$VIBEBOX_GIT_HELP_AUTOCORRECT"
|
|
19
20
|
|
|
21
|
+
# Default global gitignore (shipped with vibebox)
|
|
22
|
+
git config --global core.excludesFile "$HOME_DIR/.local/bin/gitignore_global"
|
|
23
|
+
|
|
20
24
|
# Apply git aliases (passed as JSON in VIBEBOX_GIT_ALIASES)
|
|
25
|
+
# Node calls git config directly to avoid shell injection via alias values
|
|
21
26
|
if [[ -n "$VIBEBOX_GIT_ALIASES" ]]; then
|
|
22
27
|
echo "$VIBEBOX_GIT_ALIASES" | node -e "
|
|
28
|
+
const { execFileSync } = require('child_process');
|
|
23
29
|
const aliases = JSON.parse(require('fs').readFileSync(0, 'utf8'));
|
|
24
|
-
for (const [name, cmd] of Object.entries(aliases))
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
for (const [name, cmd] of Object.entries(aliases)) {
|
|
31
|
+
try {
|
|
32
|
+
execFileSync('git', ['config', '--global', 'alias.' + name, cmd], { stdio: 'pipe' });
|
|
33
|
+
} catch {}
|
|
34
|
+
}
|
|
35
|
+
" 2>/dev/null || true
|
|
28
36
|
fi
|
|
29
37
|
}
|
|
30
38
|
|
|
@@ -58,7 +66,7 @@ show_ssh_hint() {
|
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
# ============ User Setup Scripts ============
|
|
61
|
-
# Run once per container: global ~/.vibebox/setup.sh then local .
|
|
69
|
+
# Run once per container: global ~/.vibebox/setup.sh then local vibebox.setup.sh
|
|
62
70
|
|
|
63
71
|
run_setup_scripts() {
|
|
64
72
|
if [[ -f "$SETUP_MARKER" ]]; then
|
|
@@ -67,35 +75,33 @@ run_setup_scripts() {
|
|
|
67
75
|
|
|
68
76
|
# Global setup (runs in all sandboxes)
|
|
69
77
|
local global_setup="$HOME_DIR/.vibebox/setup.sh"
|
|
70
|
-
if [[ -
|
|
78
|
+
if [[ -f "$global_setup" ]]; then
|
|
71
79
|
echo "[vibebox] Running global setup..."
|
|
72
|
-
"$global_setup"
|
|
80
|
+
if [[ -x "$global_setup" ]]; then
|
|
81
|
+
"$global_setup" || echo "[vibebox] Global setup failed (continuing)"
|
|
82
|
+
else
|
|
83
|
+
zsh "$global_setup" || echo "[vibebox] Global setup failed (continuing)"
|
|
84
|
+
fi
|
|
73
85
|
fi
|
|
74
86
|
|
|
75
87
|
# Local setup (project-specific)
|
|
76
|
-
local local_setup="$WORKSPACE
|
|
77
|
-
if [[ -
|
|
88
|
+
local local_setup="$WORKSPACE/vibebox.setup.sh"
|
|
89
|
+
if [[ -f "$local_setup" ]]; then
|
|
78
90
|
echo "[vibebox] Running local setup..."
|
|
79
|
-
"$local_setup"
|
|
91
|
+
if [[ -x "$local_setup" ]]; then
|
|
92
|
+
"$local_setup" || echo "[vibebox] Local setup failed (continuing)"
|
|
93
|
+
else
|
|
94
|
+
zsh "$local_setup" || echo "[vibebox] Local setup failed (continuing)"
|
|
95
|
+
fi
|
|
80
96
|
fi
|
|
81
97
|
|
|
82
98
|
mkdir -p "$(dirname "$SETUP_MARKER")"
|
|
83
99
|
touch "$SETUP_MARKER"
|
|
84
100
|
}
|
|
85
101
|
|
|
86
|
-
# ============ SSH Agent Socket Fix ============
|
|
87
|
-
# Docker on macOS mounts sockets as root:root, fix permissions if needed
|
|
88
|
-
|
|
89
|
-
fix_ssh_socket() {
|
|
90
|
-
if [[ -S /ssh-agent ]] && ! [[ -r /ssh-agent ]]; then
|
|
91
|
-
sudo chmod 666 /ssh-agent 2>/dev/null || true
|
|
92
|
-
fi
|
|
93
|
-
}
|
|
94
|
-
|
|
95
102
|
# ============ Main ============
|
|
96
103
|
|
|
97
104
|
# Run setup on container start
|
|
98
|
-
fix_ssh_socket
|
|
99
105
|
setup_git_config
|
|
100
106
|
setup_gh_cli
|
|
101
107
|
run_setup_scripts
|
|
@@ -1,45 +1,54 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Idle watcher - exits after TIMEOUT seconds of no
|
|
2
|
+
# Idle watcher - exits after TIMEOUT seconds of no active sessions
|
|
3
|
+
# Sessions are tracked via heartbeat files (lock files touched every 5s by host)
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
IDLE_FILE="$LOCKS_DIR/idle"
|
|
5
|
+
# Use workspace's .vibebox/locks (writable), not ~/.vibebox (read-only)
|
|
6
|
+
LOCKS_DIR="${VIBEBOX_PROJECT_ROOT:-.}/.vibebox/locks"
|
|
7
7
|
TIMEOUT=${1:-300}
|
|
8
|
+
STALE_THRESHOLD=15 # Lock considered stale if not updated in 15 seconds
|
|
8
9
|
|
|
9
|
-
# Ensure locks directory exists
|
|
10
10
|
mkdir -p "$LOCKS_DIR"
|
|
11
11
|
|
|
12
12
|
echo "Starting watcher with ${TIMEOUT}s idle timeout"
|
|
13
13
|
|
|
14
|
+
idle_start=""
|
|
15
|
+
|
|
14
16
|
while true; do
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
now=$(date +%s)
|
|
18
|
+
active_sessions=0
|
|
19
|
+
|
|
20
|
+
# Check each lock file's modification time
|
|
21
|
+
for lock in "$LOCKS_DIR"/session-*.lock; do
|
|
22
|
+
[ -f "$lock" ] || continue
|
|
23
|
+
|
|
24
|
+
# Get modification time (Linux stat syntax, container is Ubuntu)
|
|
25
|
+
mtime=$(stat -c %Y "$lock" 2>/dev/null)
|
|
26
|
+
[ -z "$mtime" ] && continue
|
|
27
|
+
|
|
28
|
+
age=$((now - mtime))
|
|
21
29
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if [ "$lock_count" -eq 0 ]; then
|
|
26
|
-
# No session locks - check/start idle timer
|
|
27
|
-
if [ -f "$IDLE_FILE" ]; then
|
|
28
|
-
idle_start=$(cat "$IDLE_FILE")
|
|
29
|
-
now=$(date +%s)
|
|
30
|
-
elapsed=$((now - idle_start))
|
|
31
|
-
if [ "$elapsed" -ge "$TIMEOUT" ]; then
|
|
32
|
-
echo "Idle timeout of ${TIMEOUT}s reached. Exiting."
|
|
33
|
-
rm -f "$IDLE_FILE"
|
|
34
|
-
exit 0
|
|
35
|
-
fi
|
|
30
|
+
if [ "$age" -lt "$STALE_THRESHOLD" ]; then
|
|
31
|
+
active_sessions=$((active_sessions + 1))
|
|
36
32
|
else
|
|
37
|
-
#
|
|
38
|
-
|
|
33
|
+
# Stale lock - session died without cleanup
|
|
34
|
+
rm -f "$lock"
|
|
35
|
+
fi
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
if [ "$active_sessions" -eq 0 ]; then
|
|
39
|
+
# No active sessions - manage idle timer
|
|
40
|
+
if [ -z "$idle_start" ]; then
|
|
41
|
+
idle_start=$now
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
elapsed=$((now - idle_start))
|
|
45
|
+
if [ "$elapsed" -ge "$TIMEOUT" ]; then
|
|
46
|
+
echo "Idle timeout of ${TIMEOUT}s reached. Exiting."
|
|
47
|
+
exit 0
|
|
39
48
|
fi
|
|
40
49
|
else
|
|
41
|
-
#
|
|
42
|
-
|
|
50
|
+
# Active sessions - reset idle timer
|
|
51
|
+
idle_start=""
|
|
43
52
|
fi
|
|
44
53
|
|
|
45
54
|
sleep 1
|
package/dist/agents/auth.js
CHANGED
|
@@ -60,7 +60,16 @@ function getCredentialStore() {
|
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
62
|
set: (service, account, value) => {
|
|
63
|
-
|
|
63
|
+
// Use execFileSync with input pipe to avoid shell injection via value
|
|
64
|
+
(0, node_child_process_1.execFileSync)("secret-tool", [
|
|
65
|
+
"store",
|
|
66
|
+
`--label=${service}`,
|
|
67
|
+
"service", service,
|
|
68
|
+
"account", account,
|
|
69
|
+
], {
|
|
70
|
+
input: value,
|
|
71
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
72
|
+
});
|
|
64
73
|
},
|
|
65
74
|
};
|
|
66
75
|
}
|
|
@@ -84,7 +84,6 @@ exports.claude = {
|
|
|
84
84
|
// Container-only mode: minimal mounts, container manages its own ~/.claude
|
|
85
85
|
if (containerOnly) {
|
|
86
86
|
return [
|
|
87
|
-
// Mount config copy (not the original, to prevent corruption)
|
|
88
87
|
"-v", `${configCopy}:${config}`,
|
|
89
88
|
];
|
|
90
89
|
}
|
|
@@ -107,7 +106,7 @@ exports.claude = {
|
|
|
107
106
|
},
|
|
108
107
|
install: (containerName) => {
|
|
109
108
|
console.log("Installing Claude Code...");
|
|
110
|
-
(0, node_child_process_1.spawnSync)("docker", ["exec", containerName, "bash", "-c", "curl -fsSL https://claude.ai/install.sh | sh"], { stdio: "inherit" });
|
|
109
|
+
(0, node_child_process_1.spawnSync)("docker", ["exec", "-u", (0, node_os_1.userInfo)().username, containerName, "bash", "-c", "curl -fsSL https://claude.ai/install.sh | sh"], { stdio: "inherit" });
|
|
111
110
|
},
|
|
112
111
|
setup: ({ workspace, containerOnly }) => {
|
|
113
112
|
const home = (0, node_os_1.homedir)();
|
|
@@ -119,8 +118,8 @@ exports.claude = {
|
|
|
119
118
|
const emptyFile = (0, node_path_1.join)(workspace, ".vibebox", "empty-file");
|
|
120
119
|
if (!(0, node_fs_1.existsSync)(emptyFile))
|
|
121
120
|
(0, node_fs_1.writeFileSync)(emptyFile, "");
|
|
122
|
-
// Create minimal config to skip onboarding
|
|
123
|
-
const
|
|
121
|
+
// Create minimal config to skip onboarding (mounted to ~/.claude.json)
|
|
122
|
+
const configFile = (0, node_path_1.join)(workspace, ".vibebox", "claude.json");
|
|
124
123
|
const minimalConfig = {
|
|
125
124
|
hasCompletedOnboarding: true,
|
|
126
125
|
projects: {
|
|
@@ -131,7 +130,7 @@ exports.claude = {
|
|
|
131
130
|
},
|
|
132
131
|
},
|
|
133
132
|
};
|
|
134
|
-
(0, node_fs_1.writeFileSync)(
|
|
133
|
+
(0, node_fs_1.writeFileSync)(configFile, JSON.stringify(minimalConfig, null, 2));
|
|
135
134
|
// Container-only mode: skip credential sync, agent will prompt for login
|
|
136
135
|
if (containerOnly)
|
|
137
136
|
return;
|
package/dist/agents/index.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.agents = void 0;
|
|
|
4
4
|
exports.detectInstalledAgents = detectInstalledAgents;
|
|
5
5
|
exports.isAgentInstalled = isAgentInstalled;
|
|
6
6
|
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const node_os_1 = require("node:os");
|
|
7
8
|
const claude_1 = require("./claude");
|
|
8
9
|
exports.agents = {
|
|
9
10
|
claude: claude_1.claude,
|
|
@@ -14,11 +15,10 @@ function detectInstalledAgents() {
|
|
|
14
15
|
.map(([name]) => name);
|
|
15
16
|
}
|
|
16
17
|
function isAgentInstalled(containerName, agent) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
18
|
+
// Use spawnSync to fully suppress stderr (execFileSync can still leak on error)
|
|
19
|
+
const result = (0, node_child_process_1.spawnSync)("docker", ["exec", "-u", (0, node_os_1.userInfo)().username, containerName, "which", agent.command], {
|
|
20
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
21
|
+
encoding: "utf8",
|
|
22
|
+
});
|
|
23
|
+
return result.status === 0;
|
|
24
24
|
}
|
package/dist/index.js
CHANGED
|
@@ -71,7 +71,7 @@ agent
|
|
|
71
71
|
containerOnly = true;
|
|
72
72
|
}
|
|
73
73
|
agentDef.setup?.({ workspace, containerOnly });
|
|
74
|
-
const containerName = ensureSandbox({ workspace, agents: [agentDef], containerOnly });
|
|
74
|
+
const containerName = await ensureSandbox({ workspace, agents: [agentDef], containerOnly });
|
|
75
75
|
agentDef.install(containerName);
|
|
76
76
|
console.log(`✓ ${agentDef.name} installed`);
|
|
77
77
|
});
|
|
@@ -98,6 +98,7 @@ async function runAgent(name, args, opts) {
|
|
|
98
98
|
// Check if we need container-only mode
|
|
99
99
|
const existing = getSandboxName(workspace);
|
|
100
100
|
const existingRunning = existing && isContainerRunning(existing);
|
|
101
|
+
// Only check if agent is installed when container is running (docker exec requires running container)
|
|
101
102
|
const needsInstall = !existingRunning || !(0, agents_1.isAgentInstalled)(existing, agent);
|
|
102
103
|
const hostInstalled = agent.isInstalledOnHost();
|
|
103
104
|
let containerOnly = false;
|
|
@@ -108,7 +109,7 @@ async function runAgent(name, args, opts) {
|
|
|
108
109
|
}
|
|
109
110
|
// Run agent setup (auth, config files)
|
|
110
111
|
agent.setup?.({ workspace, containerOnly });
|
|
111
|
-
const containerName = ensureSandbox({ workspace, agents: [agent], containerOnly });
|
|
112
|
+
const containerName = await ensureSandbox({ workspace, agents: [agent], containerOnly });
|
|
112
113
|
if (agent.versionCommand && !containerOnly) {
|
|
113
114
|
await (0, update_1.checkVersions)(containerName, agent);
|
|
114
115
|
}
|
|
@@ -126,13 +127,10 @@ async function runAgent(name, args, opts) {
|
|
|
126
127
|
agent.install(containerName);
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
|
-
const
|
|
130
|
-
name: containerName,
|
|
131
|
-
fn: () => (0, node_child_process_1.spawnSync)("docker", ["exec", "-it", containerName, agent.command, ...args], { stdio: "inherit" }),
|
|
132
|
-
});
|
|
130
|
+
const code = await withSessionLock({ name: containerName, workspace, cmd: [agent.command, ...args], interactive: true });
|
|
133
131
|
if (opts.temp)
|
|
134
132
|
await promptKeep(workspace);
|
|
135
|
-
process.exit(
|
|
133
|
+
process.exit(code);
|
|
136
134
|
}
|
|
137
135
|
async function promptContainerOnly(agentName) {
|
|
138
136
|
console.log(`\n${agentName} is not installed on your host machine.`);
|
|
@@ -161,12 +159,9 @@ commander_1.program
|
|
|
161
159
|
await (0, update_1.checkForUpdates)();
|
|
162
160
|
const workspace = process.cwd();
|
|
163
161
|
const installedAgents = setupInstalledAgents({ workspace });
|
|
164
|
-
const name = ensureSandbox({ workspace, agents: installedAgents });
|
|
162
|
+
const name = await ensureSandbox({ workspace, agents: installedAgents });
|
|
165
163
|
await (0, update_1.checkVersions)(name);
|
|
166
|
-
withSessionLock({
|
|
167
|
-
name,
|
|
168
|
-
fn: () => (0, node_child_process_1.spawnSync)("docker", ["exec", "-it", name, "/bin/zsh"], { stdio: "inherit" }),
|
|
169
|
-
});
|
|
164
|
+
await withSessionLock({ name, workspace, cmd: ["/bin/zsh"], interactive: true });
|
|
170
165
|
});
|
|
171
166
|
commander_1.program
|
|
172
167
|
.command("exec <cmd...>")
|
|
@@ -177,12 +172,9 @@ commander_1.program
|
|
|
177
172
|
await ensureDocker();
|
|
178
173
|
const workspace = process.cwd();
|
|
179
174
|
const installedAgents = setupInstalledAgents({ workspace });
|
|
180
|
-
const name = ensureSandbox({ workspace, agents: installedAgents });
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
fn: () => (0, node_child_process_1.spawnSync)("docker", ["exec", name, ...cmd], { stdio: "inherit" }),
|
|
184
|
-
});
|
|
185
|
-
process.exit(r.status ?? 0);
|
|
175
|
+
const name = await ensureSandbox({ workspace, agents: installedAgents });
|
|
176
|
+
const code = await withSessionLock({ name, workspace, cmd });
|
|
177
|
+
process.exit(code);
|
|
186
178
|
});
|
|
187
179
|
commander_1.program
|
|
188
180
|
.command("ls")
|
|
@@ -268,19 +260,16 @@ commander_1.program
|
|
|
268
260
|
commander_1.program
|
|
269
261
|
.command("rebuild")
|
|
270
262
|
.description("Force rebuild image with current host config")
|
|
271
|
-
.
|
|
263
|
+
.option("--no-cache", "Build without using Docker layer cache")
|
|
264
|
+
.action(async (opts) => {
|
|
272
265
|
await ensureDocker();
|
|
273
266
|
await (0, update_1.checkForUpdates)();
|
|
274
267
|
console.log("Rebuilding vibebox image...");
|
|
275
|
-
|
|
276
|
-
(0, node_child_process_1.execSync)("docker rmi vibebox", { stdio: "pipe" });
|
|
277
|
-
}
|
|
278
|
-
catch { }
|
|
279
|
-
buildImage();
|
|
268
|
+
buildImage({ noCache: !opts.cache });
|
|
280
269
|
console.log("Image rebuilt");
|
|
281
270
|
});
|
|
282
271
|
// ============ Ports Commands ============
|
|
283
|
-
const DEFAULT_PORTS = ["
|
|
272
|
+
const DEFAULT_PORTS = ["3000", "3001", "4173", "5000", "5173", "8080", "8081", "8545"];
|
|
284
273
|
const ports = commander_1.program.command("ports").description("Manage port mappings");
|
|
285
274
|
ports
|
|
286
275
|
.command("ls")
|
|
@@ -294,7 +283,7 @@ ports
|
|
|
294
283
|
try {
|
|
295
284
|
const mappings = getPortMappings(name);
|
|
296
285
|
// Get listening ports
|
|
297
|
-
const ssOut = (0, node_child_process_1.execFileSync)("docker", ["exec", name, "ss", "-tln"], { encoding: "utf8" });
|
|
286
|
+
const ssOut = (0, node_child_process_1.execFileSync)("docker", ["exec", "-u", (0, node_os_1.userInfo)().username, name, "ss", "-tln"], { encoding: "utf8" });
|
|
298
287
|
const listening = new Set();
|
|
299
288
|
for (const line of ssOut.split("\n").slice(1)) {
|
|
300
289
|
const match = line.match(/:(\d+)\s*$/);
|
|
@@ -336,7 +325,7 @@ ports
|
|
|
336
325
|
saveConfig(ws, cfg);
|
|
337
326
|
console.log(`Ports: ${cfg.ports.join(", ")}`);
|
|
338
327
|
if (getSandboxName(ws))
|
|
339
|
-
console.log("
|
|
328
|
+
console.log("Port changes require container restart (Docker sets ports at creation time)");
|
|
340
329
|
});
|
|
341
330
|
ports
|
|
342
331
|
.command("rm <ports...>")
|
|
@@ -349,19 +338,21 @@ ports
|
|
|
349
338
|
saveConfig(ws, cfg);
|
|
350
339
|
console.log(`Ports: ${cfg.ports.join(", ")}`);
|
|
351
340
|
if (getSandboxName(ws))
|
|
352
|
-
console.log("
|
|
341
|
+
console.log("Port changes require container restart (Docker sets ports at creation time)");
|
|
353
342
|
});
|
|
354
343
|
// ============ Customize Command ============
|
|
355
344
|
const TEMPLATES = {
|
|
356
|
-
setupGlobal: `#!/bin/
|
|
345
|
+
setupGlobal: `#!/bin/zsh
|
|
357
346
|
# Runs once when a sandbox is created (all sandboxes)
|
|
358
347
|
# Example: git clone https://github.com/YOU/dotfiles ~/.dotfiles && ~/.dotfiles/install.sh
|
|
359
|
-
#
|
|
348
|
+
# Note: sudo is disabled by default. For apt installs, use ~/.vibebox/Dockerfile instead,
|
|
349
|
+
# or enable sudo with: vibebox config sudo on
|
|
360
350
|
`,
|
|
361
|
-
setupLocal: `#!/bin/
|
|
351
|
+
setupLocal: `#!/bin/zsh
|
|
362
352
|
# Runs once when this project's sandbox is created
|
|
363
353
|
# Example: pip install -r requirements.txt
|
|
364
|
-
# Example: echo 'export DATABASE_URL=...' >> ~/.
|
|
354
|
+
# Example: echo 'export DATABASE_URL=...' >> ~/.zshrc
|
|
355
|
+
# Note: sudo is disabled by default. Enable with: vibebox config sudo on
|
|
365
356
|
`,
|
|
366
357
|
imageGlobal: `# Baked into image, applies to ALL sandboxes. Run 'vibebox rebuild' after editing.
|
|
367
358
|
FROM vibebox
|
|
@@ -391,6 +382,9 @@ function openInEditor({ file, template, isExecutable }) {
|
|
|
391
382
|
(0, node_fs_1.writeFileSync)(file, template, isExecutable ? { mode: 0o755 } : undefined);
|
|
392
383
|
console.log(`Created ${file}`);
|
|
393
384
|
}
|
|
385
|
+
else if (isExecutable) {
|
|
386
|
+
(0, node_fs_1.chmodSync)(file, 0o755);
|
|
387
|
+
}
|
|
394
388
|
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
395
389
|
const r = (0, node_child_process_1.spawnSync)(editor, [file], { stdio: "inherit" });
|
|
396
390
|
if (r.status !== 0)
|
|
@@ -401,30 +395,30 @@ customize
|
|
|
401
395
|
.command("image [scope]")
|
|
402
396
|
.description("Edit custom Dockerfile (global or local)")
|
|
403
397
|
.action(async (scope) => {
|
|
404
|
-
const targetScope = scope || await promptScope("Dockerfile", "~/.vibebox/Dockerfile", ".
|
|
398
|
+
const targetScope = scope || await promptScope("Dockerfile", "~/.vibebox/Dockerfile", "vibebox.Dockerfile");
|
|
405
399
|
if (targetScope !== "global" && targetScope !== "local") {
|
|
406
400
|
console.error("Usage: vibebox customize image <global|local>");
|
|
407
401
|
process.exit(1);
|
|
408
402
|
}
|
|
409
403
|
const isGlobal = targetScope === "global";
|
|
410
|
-
const file = (0, node_path_1.join)(
|
|
404
|
+
const file = isGlobal ? (0, node_path_1.join)((0, node_os_1.homedir)(), ".vibebox", "Dockerfile") : (0, node_path_1.join)(process.cwd(), "vibebox.Dockerfile");
|
|
411
405
|
openInEditor({ file, template: isGlobal ? TEMPLATES.imageGlobal : TEMPLATES.imageLocal });
|
|
412
|
-
console.log(`\nDockerfile
|
|
406
|
+
console.log(`\nDockerfile location: ${file}`);
|
|
413
407
|
console.log("Run 'vibebox rebuild' to apply changes.");
|
|
414
408
|
});
|
|
415
409
|
customize
|
|
416
410
|
.command("setup [scope]")
|
|
417
411
|
.description("Edit setup script (global or local)")
|
|
418
412
|
.action(async (scope) => {
|
|
419
|
-
const targetScope = scope || await promptScope("setup script", "~/.vibebox/setup.sh", ".
|
|
413
|
+
const targetScope = scope || await promptScope("setup script", "~/.vibebox/setup.sh", "vibebox.setup.sh");
|
|
420
414
|
if (targetScope !== "global" && targetScope !== "local") {
|
|
421
415
|
console.error("Usage: vibebox customize setup <global|local>");
|
|
422
416
|
process.exit(1);
|
|
423
417
|
}
|
|
424
418
|
const isGlobal = targetScope === "global";
|
|
425
|
-
const file = (0, node_path_1.join)(
|
|
419
|
+
const file = isGlobal ? (0, node_path_1.join)((0, node_os_1.homedir)(), ".vibebox", "setup.sh") : (0, node_path_1.join)(process.cwd(), "vibebox.setup.sh");
|
|
426
420
|
openInEditor({ file, template: isGlobal ? TEMPLATES.setupGlobal : TEMPLATES.setupLocal, isExecutable: true });
|
|
427
|
-
console.log(`\nSetup script
|
|
421
|
+
console.log(`\nSetup script location: ${file}`);
|
|
428
422
|
if (getSandboxName(process.cwd()))
|
|
429
423
|
console.log("Note: Runs on new sandboxes. To re-run: vibebox rm");
|
|
430
424
|
});
|
|
@@ -436,9 +430,74 @@ async function promptScope(type, globalPath, localPath) {
|
|
|
436
430
|
rl.close();
|
|
437
431
|
return answer.trim().toLowerCase();
|
|
438
432
|
}
|
|
433
|
+
// ============ Config Command ============
|
|
434
|
+
const config = commander_1.program.command("config").description("Manage sandbox configuration");
|
|
435
|
+
config
|
|
436
|
+
.command("sudo <on|off>")
|
|
437
|
+
.description("Enable or disable sudo access in sandbox (requires rebuild)")
|
|
438
|
+
.action((value) => {
|
|
439
|
+
const ws = process.cwd();
|
|
440
|
+
const cfg = loadConfig(ws);
|
|
441
|
+
if (value === "on") {
|
|
442
|
+
cfg.sudo = true;
|
|
443
|
+
saveConfig(ws, cfg);
|
|
444
|
+
console.log("Sudo enabled for this workspace");
|
|
445
|
+
}
|
|
446
|
+
else if (value === "off") {
|
|
447
|
+
cfg.sudo = false;
|
|
448
|
+
saveConfig(ws, cfg);
|
|
449
|
+
console.log("Sudo disabled for this workspace");
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
console.error("Usage: vibebox config sudo <on|off>");
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
if (getSandboxName(ws))
|
|
456
|
+
console.log("Restart for changes: vibebox stop && vibebox enter");
|
|
457
|
+
});
|
|
458
|
+
config
|
|
459
|
+
.command("show")
|
|
460
|
+
.description("Show current configuration")
|
|
461
|
+
.action(() => {
|
|
462
|
+
const ws = process.cwd();
|
|
463
|
+
const cfg = loadConfig(ws);
|
|
464
|
+
console.log("Workspace:", ws);
|
|
465
|
+
console.log("Config:", JSON.stringify(cfg, null, 2));
|
|
466
|
+
});
|
|
439
467
|
// ============ Help Display & Parse ============
|
|
440
468
|
// Only parse CLI when run directly (not when imported)
|
|
441
469
|
if (require.main === module) {
|
|
470
|
+
const warningFile = (0, node_path_1.join)((0, node_os_1.homedir)(), ".vibebox", ".accepted-warning");
|
|
471
|
+
if (!(0, node_fs_1.existsSync)(warningFile)) {
|
|
472
|
+
const red = "\x1b[31m";
|
|
473
|
+
const reset = "\x1b[0m";
|
|
474
|
+
console.log(`
|
|
475
|
+
${red}==================================
|
|
476
|
+
WARNING: EXPERIMENTAL SOFTWARE
|
|
477
|
+
==================================${reset}
|
|
478
|
+
|
|
479
|
+
This is alpha software with potential security holes.
|
|
480
|
+
Use at your own risk.
|
|
481
|
+
`);
|
|
482
|
+
const rl = (0, node_readline_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
483
|
+
rl.question(`Type ${red}Accept${reset} to continue: `, (answer) => {
|
|
484
|
+
rl.close();
|
|
485
|
+
if (answer.toLowerCase() !== "accept") {
|
|
486
|
+
console.log("Goodbye.");
|
|
487
|
+
process.exit(1);
|
|
488
|
+
}
|
|
489
|
+
const dir = (0, node_path_1.dirname)(warningFile);
|
|
490
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
491
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
492
|
+
(0, node_fs_1.writeFileSync)(warningFile, new Date().toISOString());
|
|
493
|
+
runCli();
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
else {
|
|
497
|
+
runCli();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function runCli() {
|
|
442
501
|
const isSubcommand = process.argv.length > 2 && !process.argv[2].startsWith("-");
|
|
443
502
|
if (process.argv.length === 2 ||
|
|
444
503
|
(!isSubcommand && (process.argv.includes("--help") || process.argv.includes("-h")))) {
|
|
@@ -448,12 +507,11 @@ if (require.main === module) {
|
|
|
448
507
|
}
|
|
449
508
|
// ============ Library code =========
|
|
450
509
|
// ============ Constants ============
|
|
451
|
-
const LOCKS_DIR = `${(0, node_os_1.homedir)()}/.vibebox/locks`;
|
|
452
|
-
const DETACHED_LOCK = `${LOCKS_DIR}/detached.lock`;
|
|
453
510
|
const WATCHER_SCRIPT = `${(0, node_os_1.homedir)()}/.local/bin/watcher.sh`;
|
|
454
511
|
const IDLE_TIMEOUT = 300; // 5 minutes
|
|
455
|
-
|
|
456
|
-
|
|
512
|
+
const HEARTBEAT_INTERVAL = 5000; // 5 seconds
|
|
513
|
+
function loadGlobalConfig() {
|
|
514
|
+
const p = (0, node_path_1.join)((0, node_os_1.homedir)(), ".vibebox", "config.json");
|
|
457
515
|
if (!(0, node_fs_1.existsSync)(p))
|
|
458
516
|
return {};
|
|
459
517
|
try {
|
|
@@ -463,11 +521,28 @@ function loadConfig(ws) {
|
|
|
463
521
|
return {};
|
|
464
522
|
}
|
|
465
523
|
}
|
|
524
|
+
function loadLocalConfig(ws) {
|
|
525
|
+
const p = (0, node_path_1.join)(ws, "vibebox.config.json");
|
|
526
|
+
if (!(0, node_fs_1.existsSync)(p))
|
|
527
|
+
return {};
|
|
528
|
+
try {
|
|
529
|
+
return JSON.parse((0, node_fs_1.readFileSync)(p, "utf8"));
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
return {};
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function loadConfig(ws) {
|
|
536
|
+
const global = loadGlobalConfig();
|
|
537
|
+
const local = loadLocalConfig(ws);
|
|
538
|
+
// Local overrides global (undefined means "use global default")
|
|
539
|
+
return {
|
|
540
|
+
...global,
|
|
541
|
+
...Object.fromEntries(Object.entries(local).filter(([_, v]) => v !== undefined)),
|
|
542
|
+
};
|
|
543
|
+
}
|
|
466
544
|
function saveConfig(ws, cfg) {
|
|
467
|
-
|
|
468
|
-
if (!(0, node_fs_1.existsSync)(dir))
|
|
469
|
-
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
470
|
-
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, "config.json"), JSON.stringify(cfg, null, 2));
|
|
545
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(ws, "vibebox.config.json"), JSON.stringify(cfg, null, 2));
|
|
471
546
|
}
|
|
472
547
|
function setupInstalledAgents({ workspace }) {
|
|
473
548
|
const installedAgentNames = (0, agents_1.detectInstalledAgents)();
|
|
@@ -479,40 +554,58 @@ function setupInstalledAgents({ workspace }) {
|
|
|
479
554
|
}
|
|
480
555
|
// ============ Helpers ============
|
|
481
556
|
function getWorkspaceInode(workspace) {
|
|
482
|
-
|
|
483
|
-
|
|
557
|
+
// Use execFileSync to avoid shell injection via workspace path
|
|
558
|
+
const args = process.platform === "darwin"
|
|
559
|
+
? ["-f", "%i", workspace]
|
|
560
|
+
: ["-c", "%i", workspace];
|
|
561
|
+
return (0, node_child_process_1.execFileSync)("stat", args, { encoding: "utf8" }).trim();
|
|
484
562
|
}
|
|
485
|
-
function withSessionLock({ name,
|
|
563
|
+
async function withSessionLock({ name, workspace, cmd, interactive = false }) {
|
|
486
564
|
const sessionId = (0, node_crypto_1.randomBytes)(8).toString("hex");
|
|
487
|
-
|
|
488
|
-
(0,
|
|
489
|
-
(0,
|
|
490
|
-
|
|
491
|
-
|
|
565
|
+
// Use workspace's .vibebox/locks for session locks (writable)
|
|
566
|
+
const locksDir = (0, node_path_1.join)(workspace, ".vibebox", "locks");
|
|
567
|
+
const lockFile = (0, node_path_1.join)(locksDir, `session-${sessionId}.lock`);
|
|
568
|
+
const user = (0, node_os_1.userInfo)().username;
|
|
569
|
+
// Retry first exec to handle race between docker run and container readiness
|
|
570
|
+
const maxRetries = 20;
|
|
571
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
572
|
+
try {
|
|
573
|
+
(0, node_child_process_1.execFileSync)("docker", ["exec", "-u", user, name, "mkdir", "-p", locksDir], { stdio: "pipe" });
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
catch (e) {
|
|
577
|
+
const isNotRunning = e instanceof Error && e.message.includes("is not running");
|
|
578
|
+
if (attempt === maxRetries - 1 || !isNotRunning)
|
|
579
|
+
throw e;
|
|
580
|
+
(0, node_child_process_1.spawnSync)("sleep", ["0.1"]);
|
|
581
|
+
}
|
|
492
582
|
}
|
|
493
|
-
|
|
494
|
-
|
|
583
|
+
(0, node_child_process_1.execFileSync)("docker", ["exec", "-u", user, name, "touch", lockFile], { stdio: "pipe" });
|
|
584
|
+
// Heartbeat: touch lock file periodically so watcher knows session is alive
|
|
585
|
+
const heartbeat = setInterval(() => {
|
|
495
586
|
try {
|
|
496
|
-
(0, node_child_process_1.execFileSync)("docker", ["exec",
|
|
587
|
+
(0, node_child_process_1.execFileSync)("docker", ["exec", "-u", user, name, "touch", lockFile], { stdio: "pipe" });
|
|
497
588
|
}
|
|
498
589
|
catch { } // container stopped
|
|
499
|
-
|
|
500
|
-
|
|
590
|
+
}, HEARTBEAT_INTERVAL);
|
|
591
|
+
try {
|
|
592
|
+
// Use spawn (async) so event loop keeps running for heartbeat
|
|
593
|
+
const dockerArgs = ["exec", "-u", user, ...(interactive ? ["-it"] : []), name, ...cmd];
|
|
594
|
+
const child = (0, node_child_process_1.spawn)("docker", dockerArgs, { stdio: "inherit" });
|
|
595
|
+
const code = await new Promise((resolve) => {
|
|
596
|
+
child.on("close", (code) => resolve(code ?? 0));
|
|
597
|
+
});
|
|
598
|
+
return code;
|
|
599
|
+
}
|
|
600
|
+
finally {
|
|
601
|
+
clearInterval(heartbeat);
|
|
501
602
|
try {
|
|
502
|
-
|
|
503
|
-
const locks = out.split("\n").filter((f) => f.startsWith("session-") && f.endsWith(".lock"));
|
|
504
|
-
hasOthers = locks.filter((f) => f !== `session-${sessionId}.lock`).length > 0;
|
|
603
|
+
(0, node_child_process_1.execFileSync)("docker", ["exec", "-u", user, name, "rm", "-f", lockFile], { stdio: "pipe" });
|
|
505
604
|
}
|
|
506
605
|
catch { } // container stopped
|
|
507
|
-
if (!hasOthers) {
|
|
508
|
-
try {
|
|
509
|
-
(0, node_child_process_1.execFileSync)("docker", ["exec", name, "rm", "-f", DETACHED_LOCK], { stdio: "pipe" });
|
|
510
|
-
}
|
|
511
|
-
catch { } // container stopped
|
|
512
|
-
}
|
|
513
606
|
}
|
|
514
607
|
}
|
|
515
|
-
function buildImage() {
|
|
608
|
+
function buildImage({ noCache = false } = {}) {
|
|
516
609
|
const info = (0, node_os_1.userInfo)();
|
|
517
610
|
const nodeVersion = (0, node_child_process_1.execSync)("node --version", { encoding: "utf8" }).trim().replace(/^v/, "");
|
|
518
611
|
const npmVersion = (0, node_child_process_1.execSync)("npm --version", { encoding: "utf8" }).trim();
|
|
@@ -527,6 +620,7 @@ function buildImage() {
|
|
|
527
620
|
const buildHash = (0, update_1.hashString)(buildFiles.map(f => (0, node_fs_1.readFileSync)((0, node_path_1.join)(contextDir, f), "utf8")).join("\n"));
|
|
528
621
|
const r = (0, node_child_process_1.spawnSync)("docker", [
|
|
529
622
|
"build", "-t", "vibebox",
|
|
623
|
+
...(noCache ? ["--no-cache"] : []),
|
|
530
624
|
"--build-arg", `NODE_VERSION=${nodeVersion}`,
|
|
531
625
|
"--build-arg", `NPM_VERSION=${npmVersion}`,
|
|
532
626
|
"--build-arg", `LOCAL_USER=${info.username}`,
|
|
@@ -549,28 +643,28 @@ function getImageHash(tag) {
|
|
|
549
643
|
return null;
|
|
550
644
|
}
|
|
551
645
|
}
|
|
552
|
-
function buildCustomImage({ tag,
|
|
553
|
-
|
|
554
|
-
if (!(0, node_fs_1.existsSync)(dockerfilePath))
|
|
646
|
+
function buildCustomImage({ tag, dockerfile, fromTag }) {
|
|
647
|
+
if (!(0, node_fs_1.existsSync)(dockerfile))
|
|
555
648
|
return false;
|
|
556
|
-
const content = (0, node_fs_1.readFileSync)(
|
|
649
|
+
const content = (0, node_fs_1.readFileSync)(dockerfile, "utf8");
|
|
557
650
|
const hash = (0, update_1.hashString)(content + fromTag);
|
|
558
651
|
if (getImageHash(tag) === hash)
|
|
559
652
|
return true; // Already up to date
|
|
560
653
|
console.log(`Building ${tag}...`);
|
|
561
|
-
const
|
|
654
|
+
const contextDir = (0, node_path_1.dirname)(dockerfile);
|
|
655
|
+
const r = (0, node_child_process_1.spawnSync)("docker", ["build", "-t", tag, "-f", dockerfile, "--label", `vibebox.hash=${hash}`, contextDir], { stdio: "inherit" });
|
|
562
656
|
return r.status === 0;
|
|
563
657
|
}
|
|
564
658
|
function getImageTag({ workspace }) {
|
|
565
|
-
const
|
|
566
|
-
const
|
|
567
|
-
const hasGlobal = (0, node_fs_1.existsSync)(
|
|
568
|
-
const hasLocal = (0, node_fs_1.existsSync)(
|
|
659
|
+
const globalDockerfile = (0, node_path_1.join)((0, node_os_1.homedir)(), ".vibebox", "Dockerfile");
|
|
660
|
+
const localDockerfile = (0, node_path_1.join)(workspace, "vibebox.Dockerfile");
|
|
661
|
+
const hasGlobal = (0, node_fs_1.existsSync)(globalDockerfile);
|
|
662
|
+
const hasLocal = (0, node_fs_1.existsSync)(localDockerfile);
|
|
569
663
|
const wsHash = (0, update_1.hashString)(workspace).slice(0, 8);
|
|
570
664
|
if (hasGlobal)
|
|
571
|
-
buildCustomImage({ tag: "vibebox:user",
|
|
665
|
+
buildCustomImage({ tag: "vibebox:user", dockerfile: globalDockerfile, fromTag: "vibebox" });
|
|
572
666
|
if (hasLocal)
|
|
573
|
-
buildCustomImage({ tag: `vibebox:${wsHash}`,
|
|
667
|
+
buildCustomImage({ tag: `vibebox:${wsHash}`, dockerfile: localDockerfile, fromTag: hasGlobal ? "vibebox:user" : "vibebox" });
|
|
574
668
|
if (hasLocal)
|
|
575
669
|
return `vibebox:${wsHash}`;
|
|
576
670
|
if (hasGlobal)
|
|
@@ -630,7 +724,7 @@ function writePortMappings(workspace, name) {
|
|
|
630
724
|
const parsed = [...mappings].map(([c, h]) => `${c}:${h}`).join("\n");
|
|
631
725
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(vibeboxDir, "port-mappings.txt"), parsed);
|
|
632
726
|
}
|
|
633
|
-
function ensureSandbox({ workspace, agents: requestedAgents = [], containerOnly = false, }) {
|
|
727
|
+
async function ensureSandbox({ workspace, agents: requestedAgents = [], containerOnly = false, }) {
|
|
634
728
|
const existing = getSandboxName(workspace);
|
|
635
729
|
if (existing) {
|
|
636
730
|
try {
|
|
@@ -641,13 +735,16 @@ function ensureSandbox({ workspace, agents: requestedAgents = [], containerOnly
|
|
|
641
735
|
(0, node_child_process_1.execFileSync)("docker", ["start", existing], { stdio: "pipe" });
|
|
642
736
|
writePortMappings(workspace, existing);
|
|
643
737
|
}
|
|
644
|
-
// Verify container is
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
738
|
+
// Verify container is actually usable (not just "running" state but able to exec)
|
|
739
|
+
const user = (0, node_os_1.userInfo)().username;
|
|
740
|
+
const maxRetries = 10;
|
|
741
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
742
|
+
const result = (0, node_child_process_1.spawnSync)("docker", ["exec", "-u", user, existing, "true"], { stdio: "pipe" });
|
|
743
|
+
if (result.status === 0)
|
|
744
|
+
return existing;
|
|
745
|
+
(0, node_child_process_1.spawnSync)("sleep", ["0.2"]);
|
|
746
|
+
}
|
|
747
|
+
// Container not usable, remove and recreate
|
|
651
748
|
(0, node_child_process_1.execFileSync)("docker", ["rm", "-f", existing], { stdio: "pipe" });
|
|
652
749
|
}
|
|
653
750
|
catch {
|
|
@@ -670,10 +767,12 @@ function ensureSandbox({ workspace, agents: requestedAgents = [], containerOnly
|
|
|
670
767
|
(0, node_fs_1.writeFileSync)(emptyFile, "");
|
|
671
768
|
const home = (0, node_os_1.homedir)();
|
|
672
769
|
const inode = getWorkspaceInode(workspace);
|
|
673
|
-
const
|
|
770
|
+
const cfg = loadConfig(workspace);
|
|
771
|
+
const ports = cfg.ports ?? DEFAULT_PORTS;
|
|
674
772
|
// ---- Agent args ----
|
|
675
773
|
const agentArgs = requestedAgents.flatMap((agent) => agent.dockerArgs({ workspace, home, containerOnly }));
|
|
676
774
|
// ---- Symlinks ----
|
|
775
|
+
// Scan for symlinks in mounted directories
|
|
677
776
|
const symlinkMounts = new Set();
|
|
678
777
|
const scanForSymlinks = (dir) => {
|
|
679
778
|
try {
|
|
@@ -703,27 +802,46 @@ function ensureSandbox({ workspace, agents: requestedAgents = [], containerOnly
|
|
|
703
802
|
}
|
|
704
803
|
}
|
|
705
804
|
}
|
|
805
|
+
// If symlinks found, show them and ask user
|
|
806
|
+
if (symlinkMounts.size > 0) {
|
|
807
|
+
console.log("\nSymlinks found pointing outside workspace:");
|
|
808
|
+
for (const s of symlinkMounts)
|
|
809
|
+
console.log(` \x1b[2m${s}\x1b[0m`);
|
|
810
|
+
const mount = await promptConfirm("\nMount these paths?");
|
|
811
|
+
if (!mount) {
|
|
812
|
+
symlinkMounts.clear();
|
|
813
|
+
console.log("Symlinks not mounted.");
|
|
814
|
+
}
|
|
815
|
+
}
|
|
706
816
|
// Agent names for label
|
|
707
817
|
const agentNames = requestedAgents.map((a) => a.name).join(",") || "none";
|
|
708
818
|
// Generate sandbox name
|
|
709
819
|
const now = new Date();
|
|
710
820
|
const name = `vibebox-${now.toISOString().slice(0, 10)}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}${String(now.getSeconds()).padStart(2, "0")}`;
|
|
711
821
|
// ---- SSH ----
|
|
712
|
-
//
|
|
822
|
+
// Opt-in via config (default: false)
|
|
823
|
+
// macOS: Docker Desktop runs in a VM, can't mount host socket directly.
|
|
824
|
+
// Use Docker's built-in forwarding via /run/host-services/ssh-auth.sock
|
|
825
|
+
// Linux: Mount host socket directly
|
|
826
|
+
// Windows: Named pipes (\\.\pipe\...) can't be mounted with -v
|
|
713
827
|
const sshAuthSock = process.env.SSH_AUTH_SOCK;
|
|
714
|
-
const sshArgs =
|
|
715
|
-
|
|
716
|
-
|
|
828
|
+
const sshArgs = !cfg.sshAgent ? [] :
|
|
829
|
+
process.platform === "darwin" && sshAuthSock
|
|
830
|
+
? ["-v", "/run/host-services/ssh-auth.sock:/ssh-agent", "-e", "SSH_AUTH_SOCK=/ssh-agent"]
|
|
831
|
+
: process.platform !== "win32" && sshAuthSock && (0, node_fs_1.existsSync)(sshAuthSock)
|
|
832
|
+
? ["-v", `${sshAuthSock}:/ssh-agent`, "-e", "SSH_AUTH_SOCK=/ssh-agent"]
|
|
833
|
+
: [];
|
|
717
834
|
// Global vibebox config dir (for setup.sh)
|
|
718
835
|
const vibeboxGlobalDir = (0, node_path_1.join)(home, ".vibebox");
|
|
719
836
|
const vibeboxGlobalArgs = (0, node_fs_1.existsSync)(vibeboxGlobalDir)
|
|
720
837
|
? ["-v", `${vibeboxGlobalDir}:${vibeboxGlobalDir}:ro`]
|
|
721
838
|
: [];
|
|
722
839
|
// ---- GitHub CLI ----
|
|
840
|
+
// Opt-in via config (default: false)
|
|
723
841
|
const ghConfigDir = process.platform === "win32" && process.env.APPDATA
|
|
724
842
|
? (0, node_path_1.join)(process.env.APPDATA, "GitHub CLI")
|
|
725
843
|
: (0, node_path_1.join)(home, ".config", "gh");
|
|
726
|
-
const ghArgs = (0, node_fs_1.existsSync)((0, node_path_1.join)(ghConfigDir, "hosts.yml"))
|
|
844
|
+
const ghArgs = cfg.ghAuth && (0, node_fs_1.existsSync)((0, node_path_1.join)(ghConfigDir, "hosts.yml"))
|
|
727
845
|
? ["-v", `${ghConfigDir}:${(0, node_path_1.join)(home, ".config", "gh")}:ro`]
|
|
728
846
|
: [];
|
|
729
847
|
// ---- Git config ----
|
|
@@ -760,10 +878,23 @@ function ensureSandbox({ workspace, agents: requestedAgents = [], containerOnly
|
|
|
760
878
|
}
|
|
761
879
|
}
|
|
762
880
|
catch { } // no aliases
|
|
881
|
+
// ---- Security ----
|
|
882
|
+
const sudoArgs = cfg.sudo ? ["-e", "VIBEBOX_SUDO=1"] : [];
|
|
763
883
|
// ---- Docker run ----
|
|
764
884
|
const args = [
|
|
765
885
|
"run", "-d",
|
|
766
886
|
"--name", name,
|
|
887
|
+
// Security hardening: drop all capabilities, add back only what's needed
|
|
888
|
+
// See: man 7 capabilities
|
|
889
|
+
"--security-opt", "no-new-privileges",
|
|
890
|
+
"--cap-drop", "ALL",
|
|
891
|
+
// Required for entrypoint (runs as root, then drops to user via gosu):
|
|
892
|
+
"--cap-add", "CHOWN", // chown on SSH socket
|
|
893
|
+
"--cap-add", "SETUID", // gosu to switch user
|
|
894
|
+
"--cap-add", "SETGID", // gosu to switch group
|
|
895
|
+
"--cap-add", "DAC_OVERRIDE", // write to /etc/sudoers.d
|
|
896
|
+
"--cap-add", "FOWNER", // operate on files regardless of owner
|
|
897
|
+
"--cap-add", "KILL", // send signals to processes
|
|
767
898
|
// Labels for container lookup
|
|
768
899
|
"--label", "docker/sandbox=true",
|
|
769
900
|
"--label", `com.docker.sandbox.agent=${agentNames}`,
|
|
@@ -780,6 +911,8 @@ function ensureSandbox({ workspace, agents: requestedAgents = [], containerOnly
|
|
|
780
911
|
// Environment
|
|
781
912
|
"-e", `TZ=${process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone}`,
|
|
782
913
|
"-e", `VIBEBOX_PROJECT_ROOT=${workspace}`,
|
|
914
|
+
// Sudo opt-in
|
|
915
|
+
...sudoArgs,
|
|
783
916
|
// Git config env vars
|
|
784
917
|
...gitEnvArgs,
|
|
785
918
|
// Agent-specific mounts
|
|
@@ -790,8 +923,8 @@ function ensureSandbox({ workspace, agents: requestedAgents = [], containerOnly
|
|
|
790
923
|
"-v", `${workspace}:${workspace}`,
|
|
791
924
|
"-w", workspace,
|
|
792
925
|
imageTag,
|
|
793
|
-
//
|
|
794
|
-
|
|
926
|
+
// Command passed to entrypoint -> startup.sh
|
|
927
|
+
"bash", "-c", `${WATCHER_SCRIPT} ${IDLE_TIMEOUT}`,
|
|
795
928
|
];
|
|
796
929
|
const result = (0, node_child_process_1.spawnSync)("docker", args, { stdio: "pipe", encoding: "utf-8" });
|
|
797
930
|
if (result.status !== 0) {
|
package/dist/update.js
CHANGED
|
@@ -43,6 +43,7 @@ const node_readline_1 = require("node:readline");
|
|
|
43
43
|
const node_fs_1 = require("node:fs");
|
|
44
44
|
const node_crypto_1 = require("node:crypto");
|
|
45
45
|
const node_path_1 = require("node:path");
|
|
46
|
+
const node_os_1 = require("node:os");
|
|
46
47
|
function getVersion(cmd) {
|
|
47
48
|
const out = (0, node_child_process_1.execSync)(cmd, { encoding: "utf8" });
|
|
48
49
|
const match = out.match(/^(\d+\.\d+\.\d+(-[^\s]+)?)/);
|
|
@@ -101,7 +102,7 @@ function updateHost(agentCommand, version) {
|
|
|
101
102
|
}
|
|
102
103
|
function updateSandbox(sandboxName, agentCommand, version) {
|
|
103
104
|
console.log(`\nUpdating sandbox ${agentCommand}${version ? ` to ${version}` : " to latest"}...`);
|
|
104
|
-
const args = ["exec", "-it", sandboxName, agentCommand, "install"];
|
|
105
|
+
const args = ["exec", "-u", (0, node_os_1.userInfo)().username, "-it", sandboxName, agentCommand, "install"];
|
|
105
106
|
if (version)
|
|
106
107
|
args.push(version);
|
|
107
108
|
const result = (0, node_child_process_1.spawnSync)("docker", args, { stdio: "inherit" });
|
|
@@ -111,7 +112,8 @@ function updateSandbox(sandboxName, agentCommand, version) {
|
|
|
111
112
|
}
|
|
112
113
|
function verifyVersionsMatch({ sandboxName, versionCommand, successMsg }) {
|
|
113
114
|
const newHost = getVersion(versionCommand);
|
|
114
|
-
|
|
115
|
+
// Use zsh -lc to get login shell PATH (includes ~/.local/bin after install)
|
|
116
|
+
const newSandbox = getVersion(`docker exec -u ${(0, node_os_1.userInfo)().username} ${sandboxName} zsh -lc '${versionCommand}'`);
|
|
115
117
|
if (newHost === newSandbox) {
|
|
116
118
|
console.log(`\nSuccess! ${successMsg} ${newHost}`);
|
|
117
119
|
}
|
|
@@ -211,7 +213,7 @@ async function checkVersions(sandboxName, agent) {
|
|
|
211
213
|
const agentCommand = agent?.command ?? "claude";
|
|
212
214
|
try {
|
|
213
215
|
const host = getVersion(versionCommand);
|
|
214
|
-
const sandbox = getVersion(`docker exec ${sandboxName} ${versionCommand}`);
|
|
216
|
+
const sandbox = getVersion(`docker exec -u ${(0, node_os_1.userInfo)().username} ${sandboxName} zsh -lc '${versionCommand}'`);
|
|
215
217
|
if (host !== sandbox) {
|
|
216
218
|
const comparison = compareVersions(host, sandbox);
|
|
217
219
|
const higher = comparison >= 0 ? host : sandbox;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibebox",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
5
5
|
"bin": {
|
|
6
6
|
"vibebox": "./dist/index.js"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"build": "tsc",
|
|
17
17
|
"postbuild": "chmod +x dist/index.js",
|
|
18
18
|
"build:image": "docker build -t vibebox .",
|
|
19
|
-
"rebuild": "
|
|
19
|
+
"rebuild": "npm run build && node scripts/update-build-hashes.mjs && vibebox rm --all && vibebox rebuild",
|
|
20
20
|
"preversion": "npm run build && node scripts/update-build-hashes.mjs",
|
|
21
21
|
"prepublishOnly": "npm run build",
|
|
22
22
|
"test": "vitest run",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"vibebox": {
|
|
34
34
|
"version": {
|
|
35
|
-
"build": "
|
|
35
|
+
"build": "90d18f8d395b9ad5"
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
}
|