vibebox 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +85 -0
- package/LICENSE.md +7 -0
- package/README.md +118 -2
- package/container-scripts/port-monitor.sh +37 -0
- package/container-scripts/startup.sh +11 -0
- package/container-scripts/watcher.sh +46 -0
- package/dist/agents/auth.js +71 -0
- package/dist/agents/claude/index.js +176 -0
- package/dist/agents/index.js +24 -0
- package/dist/agents/types.js +2 -0
- package/dist/auth.js +11 -0
- package/dist/index.js +658 -0
- package/dist/update.js +282 -0
- package/package.json +24 -24
- package/.dockerignore +0 -13
- package/LICENSE +0 -1
package/Dockerfile
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
FROM ubuntu:24.04
|
|
2
|
+
|
|
3
|
+
ARG TZ=UTC
|
|
4
|
+
ARG NODE_VERSION=24.12.0
|
|
5
|
+
ARG NPM_VERSION=11.7.0
|
|
6
|
+
ARG LOCAL_USER=coder
|
|
7
|
+
ARG LOCAL_UID=1000
|
|
8
|
+
ARG LOCAL_GID=1000
|
|
9
|
+
ARG LOCAL_HOME=/home/coder
|
|
10
|
+
|
|
11
|
+
ENV TZ="$TZ"
|
|
12
|
+
|
|
13
|
+
# Install system dependencies
|
|
14
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
15
|
+
curl \
|
|
16
|
+
dnsutils \
|
|
17
|
+
fzf \
|
|
18
|
+
gh \
|
|
19
|
+
git \
|
|
20
|
+
gnupg2 \
|
|
21
|
+
iproute2 \
|
|
22
|
+
jq \
|
|
23
|
+
less \
|
|
24
|
+
lsof \
|
|
25
|
+
net-tools \
|
|
26
|
+
procps \
|
|
27
|
+
python3 \
|
|
28
|
+
python3-pip \
|
|
29
|
+
sudo \
|
|
30
|
+
unzip \
|
|
31
|
+
vim \
|
|
32
|
+
wget \
|
|
33
|
+
zsh \
|
|
34
|
+
&& apt-get clean \
|
|
35
|
+
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
|
36
|
+
|
|
37
|
+
# Create parent directory for non-standard home paths (e.g., /Users/g on macOS)
|
|
38
|
+
RUN mkdir -p $(dirname $LOCAL_HOME) 2>/dev/null || true
|
|
39
|
+
|
|
40
|
+
# Create group and user
|
|
41
|
+
RUN groupadd -g $LOCAL_GID $LOCAL_USER 2>/dev/null || true && \
|
|
42
|
+
GROUP_NAME=$(getent group $LOCAL_GID | cut -d: -f1) && \
|
|
43
|
+
useradd -m -s /bin/zsh -u $LOCAL_UID -g $GROUP_NAME -d $LOCAL_HOME $LOCAL_USER && \
|
|
44
|
+
echo "$LOCAL_USER ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
|
45
|
+
|
|
46
|
+
USER $LOCAL_USER
|
|
47
|
+
WORKDIR $LOCAL_HOME
|
|
48
|
+
|
|
49
|
+
# Install nvm and Node.js
|
|
50
|
+
ENV NVM_DIR="$LOCAL_HOME/.nvm"
|
|
51
|
+
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash && \
|
|
52
|
+
. "$NVM_DIR/nvm.sh" && \
|
|
53
|
+
nvm install $NODE_VERSION && \
|
|
54
|
+
nvm use $NODE_VERSION && \
|
|
55
|
+
nvm alias default $NODE_VERSION && \
|
|
56
|
+
npm install -g npm@$NPM_VERSION
|
|
57
|
+
|
|
58
|
+
# Install Claude Code and sfw
|
|
59
|
+
RUN curl -fsSL https://claude.ai/install.sh | bash && \
|
|
60
|
+
. "$NVM_DIR/nvm.sh" && npm install -g sfw
|
|
61
|
+
|
|
62
|
+
# Configure zsh
|
|
63
|
+
RUN echo 'export NVM_DIR="$HOME/.nvm"' >> $LOCAL_HOME/.zshrc && \
|
|
64
|
+
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $LOCAL_HOME/.zshrc && \
|
|
65
|
+
echo 'export PS1="vibebox:%~ %(#.#.$) "' >> $LOCAL_HOME/.zshrc && \
|
|
66
|
+
echo 'setopt PROMPT_SUBST' >> $LOCAL_HOME/.zshrc && \
|
|
67
|
+
echo 'alias npm="sfw npm"' >> $LOCAL_HOME/.zshrc && \
|
|
68
|
+
echo 'alias npx="sfw npx"' >> $LOCAL_HOME/.zshrc && \
|
|
69
|
+
echo '$HOME/.local/bin/port-monitor.sh &!' >> $LOCAL_HOME/.zshrc
|
|
70
|
+
|
|
71
|
+
# Copy container scripts
|
|
72
|
+
COPY --chown=$LOCAL_USER:$LOCAL_GID container-scripts/ $LOCAL_HOME/.local/bin/
|
|
73
|
+
RUN chmod +x $LOCAL_HOME/.local/bin/*.sh
|
|
74
|
+
|
|
75
|
+
# Environment variables
|
|
76
|
+
ENV SHELL=/bin/zsh \
|
|
77
|
+
TERM=xterm-256color \
|
|
78
|
+
COLORTERM=truecolor \
|
|
79
|
+
EDITOR=vim \
|
|
80
|
+
VISUAL=vim \
|
|
81
|
+
DEVCONTAINER=true \
|
|
82
|
+
PATH="$LOCAL_HOME/.local/bin:$LOCAL_HOME/.nvm/versions/node/v$NODE_VERSION/bin:$PATH" \
|
|
83
|
+
NODE_PATH="$LOCAL_HOME/.nvm/versions/node/v$NODE_VERSION/lib/node_modules"
|
|
84
|
+
|
|
85
|
+
CMD ["/bin/zsh", "-c", "$HOME/.local/bin/startup.sh tail -f /dev/null"]
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026-present Giuseppe Gurgone
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,119 @@
|
|
|
1
|
-
#
|
|
1
|
+
# vibebox
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A minimal CLI for running dev sandboxes and CLI agents in Docker containers. Each workspace gets its own isolated container with automatic credential sync and config isolation.
|
|
4
|
+
|
|
5
|
+
With supply chain attacks on npm becoming increasingly common, sandboxing your dev environment protects your host machine from malicious packages.
|
|
6
|
+
|
|
7
|
+
The architecture supports multiple agents. Currently only Claude Code is integrated.
|
|
8
|
+
|
|
9
|
+
[Why vibebox instead of `docker sandbox`, raw docker, or devcontainers?](#why-vibebox)
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Docker Desktop
|
|
14
|
+
- Agent CLI on host (optional - enables shared auth/config across workspaces)
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g vibebox
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Add to your shell profile (optional):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
alias claude="vibebox agent run claude"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Agent commands
|
|
32
|
+
vibebox agent run <name> [args] # Run agent in sandbox
|
|
33
|
+
vibebox agent run claude --temp # Temporary workspace, prompts to save on exit
|
|
34
|
+
vibebox agent ls # List agents with install status
|
|
35
|
+
vibebox agent install <name> # Install agent in sandbox
|
|
36
|
+
|
|
37
|
+
# Container commands
|
|
38
|
+
vibebox enter # Shell into sandbox
|
|
39
|
+
vibebox exec <cmd> # Run command in sandbox
|
|
40
|
+
vibebox ls # List sandboxes
|
|
41
|
+
vibebox stop [--all] # Stop sandbox(es)
|
|
42
|
+
vibebox rm [--all] # Remove sandbox(es)
|
|
43
|
+
vibebox rebuild # Rebuild image with current host config
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Port Management
|
|
47
|
+
|
|
48
|
+
Default ports exposed: 5173, 3000, 3001, 4173, 8080 (dynamic host allocation)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
vibebox ports list # Show current port mappings
|
|
52
|
+
vibebox ports add 4200 # Add custom port
|
|
53
|
+
vibebox ports remove 3000 # Remove port
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
When a service starts listening inside the container, you'll see:
|
|
57
|
+
```
|
|
58
|
+
● Port 5173 → http://localhost:32770
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For unmapped ports:
|
|
62
|
+
```
|
|
63
|
+
⊖ Port 9000 listening (not exposed)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Image Build
|
|
67
|
+
|
|
68
|
+
The Docker image is built to match your host environment. When you run `vibebox rebuild`, it:
|
|
69
|
+
|
|
70
|
+
1. Detects host Node.js version (`node --version`)
|
|
71
|
+
2. Detects host npm version (`npm --version`)
|
|
72
|
+
3. Detects user info (username, UID, GID, home directory)
|
|
73
|
+
4. Passes these as build arguments to Docker
|
|
74
|
+
|
|
75
|
+
This ensures the container user and environment mirrors your host setup for seamless file permissions and tooling.
|
|
76
|
+
|
|
77
|
+
## Version Management
|
|
78
|
+
|
|
79
|
+
When agent versions differ between host and sandbox:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
claude version mismatch! Host: 2.1.2, Sandbox: 2.1.0
|
|
83
|
+
|
|
84
|
+
Choose update strategy:
|
|
85
|
+
[1] Sync to higher version (2.1.2)
|
|
86
|
+
[2] Update both to latest
|
|
87
|
+
[N] Cancel
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Why vibebox?
|
|
91
|
+
|
|
92
|
+
### vs `docker sandbox`
|
|
93
|
+
|
|
94
|
+
Docker Desktop includes an experimental `docker sandbox` command for running agents (Claude Code, Gemini) in containers. Here's why vibebox takes a different approach:
|
|
95
|
+
|
|
96
|
+
- **Transparency over black box**: `docker sandbox` is opaque. You can't see what it mounts, how it configures the environment, or debug issues. vibebox is simple TypeScript you can read and modify.
|
|
97
|
+
- **No `--dangerously-skip-permissions`**: `docker sandbox` runs Claude with all permissions bypassed. vibebox respects your existing Claude settings and permission model.
|
|
98
|
+
- **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.
|
|
99
|
+
- **Works with any Docker runtime**: `docker sandbox` requires Docker Desktop. vibebox works with Docker Engine, Colima, or any docker-compliant api.
|
|
100
|
+
- **Interactive shell access**: `vibebox enter` drops you into a shell inside the container. Useful for debugging, running commands, or working alongside the agent.
|
|
101
|
+
- **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.
|
|
102
|
+
- **Persistent containers**: Containers persist between sessions. Your installed packages, build artifacts, and environment stay intact until you explicitly remove them.
|
|
103
|
+
|
|
104
|
+
### vs raw docker
|
|
105
|
+
|
|
106
|
+
- **Zero boilerplate**: No writing Dockerfiles, figuring out mount paths, or managing `docker run` flags. Just `vibebox agent run claude`.
|
|
107
|
+
- **Automatic credential sync**: Credentials are extracted from your system keychain and mounted into the container. No manual token copying.
|
|
108
|
+
- **User matching**: Container user matches your host UID/GID/home path, so file permissions just work.
|
|
109
|
+
- **Built-in port detection**: When a service starts listening, vibebox shows you the mapped URL. No guessing which host port maps where.
|
|
110
|
+
|
|
111
|
+
### vs devcontainers
|
|
112
|
+
|
|
113
|
+
- **No IDE coupling**: Devcontainers are designed for VS Code. vibebox works from any terminal.
|
|
114
|
+
- **Simpler model**: No `devcontainer.json`, no features, no lifecycle hooks. One command to start.
|
|
115
|
+
- **Agent-first**: Built specifically for CLI agents with credential mounting, not general-purpose dev environments.
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Vibebox port monitor - detects new listening ports and shows mappings
|
|
3
|
+
# Started in background by .zshrc
|
|
4
|
+
|
|
5
|
+
[[ -z "$VIBEBOX_PROJECT_ROOT" ]] && exit 0
|
|
6
|
+
|
|
7
|
+
mappings_file="$VIBEBOX_PROJECT_ROOT/.vibebox/port-mappings.txt"
|
|
8
|
+
previous=""
|
|
9
|
+
|
|
10
|
+
# Wait for shell to settle
|
|
11
|
+
sleep 2
|
|
12
|
+
|
|
13
|
+
# Get initial state (don't report existing ports)
|
|
14
|
+
previous=$(ss -tln 2>/dev/null | awk 'NR>1 {split($4,a,":"); print a[length(a)]}' | sort -nu | tr '\n' ' ')
|
|
15
|
+
|
|
16
|
+
while true; do
|
|
17
|
+
sleep 3
|
|
18
|
+
|
|
19
|
+
current=$(ss -tln 2>/dev/null | awk 'NR>1 {split($4,a,":"); print a[length(a)]}' | sort -nu | tr '\n' ' ')
|
|
20
|
+
|
|
21
|
+
# Find new ports
|
|
22
|
+
for port in $current; do
|
|
23
|
+
[[ -z "$port" || "$port" == "*" ]] && continue
|
|
24
|
+
if ! echo "$previous" | grep -qw "$port"; then
|
|
25
|
+
if [[ -f "$mappings_file" ]]; then
|
|
26
|
+
mapping=$(grep "^${port}:" "$mappings_file" 2>/dev/null)
|
|
27
|
+
if [[ -n "$mapping" ]]; then
|
|
28
|
+
echo -e "\n ● Port $port → http://localhost:${mapping#*:}"
|
|
29
|
+
else
|
|
30
|
+
echo -e "\n ⊖ Port $port listening (not exposed)"
|
|
31
|
+
fi
|
|
32
|
+
fi
|
|
33
|
+
fi
|
|
34
|
+
done
|
|
35
|
+
|
|
36
|
+
previous="$current"
|
|
37
|
+
done
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# Run command with node if the first argument contains a "-" or is not a system command. The last
|
|
5
|
+
# part inside the "{}" is a workaround for the following bug in ash/dash:
|
|
6
|
+
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=874264
|
|
7
|
+
if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ] || { [ -f "${1}" ] && ! [ -x "${1}" ]; }; then
|
|
8
|
+
set -- node "$@"
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
exec "$@"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Idle watcher - exits after TIMEOUT seconds of no lock files (except detached.lock)
|
|
3
|
+
|
|
4
|
+
LOCKS_DIR="$HOME/.vibebox/locks"
|
|
5
|
+
DETACHED_LOCK="$LOCKS_DIR/detached.lock"
|
|
6
|
+
IDLE_FILE="$LOCKS_DIR/idle"
|
|
7
|
+
TIMEOUT=${1:-300}
|
|
8
|
+
|
|
9
|
+
# Ensure locks directory exists
|
|
10
|
+
mkdir -p "$LOCKS_DIR"
|
|
11
|
+
|
|
12
|
+
echo "Starting watcher with ${TIMEOUT}s idle timeout"
|
|
13
|
+
|
|
14
|
+
while true; do
|
|
15
|
+
# If detached.lock exists, loop forever
|
|
16
|
+
if [ -f "$DETACHED_LOCK" ]; then
|
|
17
|
+
rm -f "$IDLE_FILE"
|
|
18
|
+
sleep 1
|
|
19
|
+
continue
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Count session lock files (session-*.lock)
|
|
23
|
+
lock_count=$(find "$LOCKS_DIR" -name 'session-*.lock' 2>/dev/null | wc -l)
|
|
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
|
|
36
|
+
else
|
|
37
|
+
# Start idle timer
|
|
38
|
+
date +%s > "$IDLE_FILE"
|
|
39
|
+
fi
|
|
40
|
+
else
|
|
41
|
+
# Sessions active - reset idle timer
|
|
42
|
+
rm -f "$IDLE_FILE"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
sleep 1
|
|
46
|
+
done
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCredentialStore = getCredentialStore;
|
|
4
|
+
exports.readCredentialsFile = readCredentialsFile;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const node_os_1 = require("node:os");
|
|
8
|
+
function getCredentialStore() {
|
|
9
|
+
switch ((0, node_os_1.platform)()) {
|
|
10
|
+
case "darwin":
|
|
11
|
+
return {
|
|
12
|
+
get: (service, account) => {
|
|
13
|
+
try {
|
|
14
|
+
return (0, node_child_process_1.execSync)(`security find-generic-password -a "${account}" -s "${service}" -w`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
set: (service, account, value) => {
|
|
21
|
+
const hexData = Buffer.from(value, "utf-8").toString("hex");
|
|
22
|
+
const input = `add-generic-password -U -a "${account}" -s "${service}" -X "${hexData}"\n`;
|
|
23
|
+
(0, node_child_process_1.execSync)("security -i", {
|
|
24
|
+
input,
|
|
25
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
case "win32":
|
|
30
|
+
return {
|
|
31
|
+
get: (service, account) => {
|
|
32
|
+
try {
|
|
33
|
+
const cmd = `powershell -Command "(Get-StoredCredential -Target '${service}:${account}').GetNetworkCredential().Password"`;
|
|
34
|
+
return (0, node_child_process_1.execSync)(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
set: (service, account, value) => {
|
|
41
|
+
const cmd = `powershell -Command "New-StoredCredential -Target '${service}:${account}' -Password '${value}' -Persist LocalMachine"`;
|
|
42
|
+
(0, node_child_process_1.execSync)(cmd);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
default:
|
|
46
|
+
// Linux: secret-tool (libsecret)
|
|
47
|
+
return {
|
|
48
|
+
get: (service, account) => {
|
|
49
|
+
try {
|
|
50
|
+
return (0, node_child_process_1.execSync)(`secret-tool lookup service "${service}" account "${account}"`, { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
set: (service, account, value) => {
|
|
57
|
+
(0, node_child_process_1.execSync)(`echo -n "${value}" | secret-tool store --label="${service}" service "${service}" account "${account}"`);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function readCredentialsFile(path) {
|
|
63
|
+
try {
|
|
64
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
65
|
+
return null;
|
|
66
|
+
return JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.claude = void 0;
|
|
4
|
+
exports.getCredentialPaths = getCredentialPaths;
|
|
5
|
+
const auth_1 = require("../auth");
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_os_1 = require("node:os");
|
|
9
|
+
const node_path_1 = require("node:path");
|
|
10
|
+
const node_crypto_1 = require("node:crypto");
|
|
11
|
+
const KEYCHAIN_SERVICES = [
|
|
12
|
+
"Claude Code-credentials",
|
|
13
|
+
"claude-code-credentials",
|
|
14
|
+
"claude.ai",
|
|
15
|
+
];
|
|
16
|
+
function getKeychainServiceName() {
|
|
17
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
18
|
+
if (configDir) {
|
|
19
|
+
const hash = (0, node_crypto_1.createHash)("sha256")
|
|
20
|
+
.update(configDir)
|
|
21
|
+
.digest("hex")
|
|
22
|
+
.substring(0, 8);
|
|
23
|
+
return `Claude Code-credentials-${hash}`;
|
|
24
|
+
}
|
|
25
|
+
return "Claude Code-credentials";
|
|
26
|
+
}
|
|
27
|
+
function extractFromKeychain() {
|
|
28
|
+
const store = (0, auth_1.getCredentialStore)();
|
|
29
|
+
const user = (0, node_os_1.userInfo)().username;
|
|
30
|
+
for (const svc of KEYCHAIN_SERVICES) {
|
|
31
|
+
const data = store.get(svc, user);
|
|
32
|
+
if (data) {
|
|
33
|
+
try {
|
|
34
|
+
const creds = JSON.parse(data);
|
|
35
|
+
if (creds.claudeAiOauth?.accessToken)
|
|
36
|
+
return creds;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function compareFreshness(first, second) {
|
|
46
|
+
const firstExpiry = first?.claudeAiOauth?.expiresAt;
|
|
47
|
+
const secondExpiry = second?.claudeAiOauth?.expiresAt;
|
|
48
|
+
if (!first?.claudeAiOauth?.accessToken && !second?.claudeAiOauth?.accessToken) {
|
|
49
|
+
return "unknown";
|
|
50
|
+
}
|
|
51
|
+
if (!first?.claudeAiOauth?.accessToken)
|
|
52
|
+
return "second";
|
|
53
|
+
if (!second?.claudeAiOauth?.accessToken)
|
|
54
|
+
return "first";
|
|
55
|
+
if (firstExpiry === undefined && secondExpiry === undefined)
|
|
56
|
+
return "equal";
|
|
57
|
+
if (firstExpiry === undefined)
|
|
58
|
+
return "second";
|
|
59
|
+
if (secondExpiry === undefined)
|
|
60
|
+
return "first";
|
|
61
|
+
if (firstExpiry > secondExpiry)
|
|
62
|
+
return "first";
|
|
63
|
+
if (secondExpiry > firstExpiry)
|
|
64
|
+
return "second";
|
|
65
|
+
return "equal";
|
|
66
|
+
}
|
|
67
|
+
exports.claude = {
|
|
68
|
+
name: "claude",
|
|
69
|
+
command: "claude",
|
|
70
|
+
configDir: (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude"),
|
|
71
|
+
isInstalledOnHost: () => {
|
|
72
|
+
try {
|
|
73
|
+
(0, node_child_process_1.execSync)("claude --version", { stdio: "pipe" });
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
dockerArgs: ({ workspace, home, containerOnly }) => {
|
|
81
|
+
const claudeDir = (0, node_path_1.join)(home, ".claude");
|
|
82
|
+
const configCopy = (0, node_path_1.join)(workspace, ".vibebox", "claude.json");
|
|
83
|
+
const config = (0, node_path_1.join)(home, ".claude.json");
|
|
84
|
+
// Container-only mode: minimal mounts, container manages its own ~/.claude
|
|
85
|
+
if (containerOnly) {
|
|
86
|
+
return [
|
|
87
|
+
// Mount config copy (not the original, to prevent corruption)
|
|
88
|
+
"-v", `${configCopy}:${config}`,
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
// Host mode: mount ~/.claude with selective hiding for isolation
|
|
92
|
+
const projectFolder = workspace.replace(/\//g, "-");
|
|
93
|
+
const emptyDir = (0, node_path_1.join)(workspace, ".vibebox", "empty");
|
|
94
|
+
const emptyFile = (0, node_path_1.join)(workspace, ".vibebox", "empty-file");
|
|
95
|
+
const credentials = (0, node_path_1.join)(claudeDir, ".credentials.json");
|
|
96
|
+
return [
|
|
97
|
+
// Mount ~/.claude with selective hiding
|
|
98
|
+
"-v", `${claudeDir}:${claudeDir}`,
|
|
99
|
+
"-v", `${emptyDir}:${claudeDir}/projects`,
|
|
100
|
+
"-v", `${emptyFile}:${claudeDir}/history.jsonl`,
|
|
101
|
+
"-v", `${claudeDir}/projects/${projectFolder}:${claudeDir}/projects/${projectFolder}`,
|
|
102
|
+
// Mount credentials
|
|
103
|
+
"-v", `${credentials}:${credentials}`,
|
|
104
|
+
// Mount config copy (not the original, to prevent corruption)
|
|
105
|
+
"-v", `${configCopy}:${config}`,
|
|
106
|
+
];
|
|
107
|
+
},
|
|
108
|
+
install: (containerName) => {
|
|
109
|
+
console.log("Installing Claude Code...");
|
|
110
|
+
(0, node_child_process_1.execSync)(`docker exec ${containerName} bash -c "curl -fsSL https://claude.ai/install.sh | sh"`, { stdio: "inherit" });
|
|
111
|
+
},
|
|
112
|
+
setup: ({ workspace, containerOnly }) => {
|
|
113
|
+
const home = (0, node_os_1.homedir)();
|
|
114
|
+
const claudeDir = (0, node_path_1.join)(home, ".claude");
|
|
115
|
+
const credFile = (0, node_path_1.join)(claudeDir, ".credentials.json");
|
|
116
|
+
// Ensure directories exist
|
|
117
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.join)(workspace, ".vibebox", "empty"), { recursive: true });
|
|
118
|
+
// Create empty overlay file
|
|
119
|
+
const emptyFile = (0, node_path_1.join)(workspace, ".vibebox", "empty-file");
|
|
120
|
+
if (!(0, node_fs_1.existsSync)(emptyFile))
|
|
121
|
+
(0, node_fs_1.writeFileSync)(emptyFile, "");
|
|
122
|
+
// Create minimal config to skip onboarding and trust prompts
|
|
123
|
+
const configCopy = (0, node_path_1.join)(workspace, ".vibebox", "claude.json");
|
|
124
|
+
const minimalConfig = {
|
|
125
|
+
hasCompletedOnboarding: true,
|
|
126
|
+
projects: {
|
|
127
|
+
[workspace]: {
|
|
128
|
+
allowedTools: [],
|
|
129
|
+
hasTrustDialogAccepted: true,
|
|
130
|
+
hasCompletedProjectOnboarding: true,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
(0, node_fs_1.writeFileSync)(configCopy, JSON.stringify(minimalConfig, null, 2));
|
|
135
|
+
// Container-only mode: skip credential sync, agent will prompt for login
|
|
136
|
+
if (containerOnly)
|
|
137
|
+
return;
|
|
138
|
+
// Host mode: sync credentials from system credential store
|
|
139
|
+
(0, node_fs_1.mkdirSync)(claudeDir, { recursive: true });
|
|
140
|
+
// Clean up if credentials file is a directory (Claude bug)
|
|
141
|
+
if ((0, node_fs_1.existsSync)(credFile) && (0, node_fs_1.lstatSync)(credFile).isDirectory()) {
|
|
142
|
+
(0, node_fs_1.rmSync)(credFile, { recursive: true, force: true });
|
|
143
|
+
}
|
|
144
|
+
// Sync credentials from system credential store (macOS only for now)
|
|
145
|
+
if (process.platform === "darwin") {
|
|
146
|
+
const keychainCreds = extractFromKeychain();
|
|
147
|
+
const fileCreds = (0, auth_1.readCredentialsFile)(credFile);
|
|
148
|
+
const freshness = compareFreshness(keychainCreds, fileCreds);
|
|
149
|
+
if (freshness === "first" || freshness === "equal") {
|
|
150
|
+
if (!keychainCreds)
|
|
151
|
+
throw new Error("No credentials. Run: claude auth login");
|
|
152
|
+
(0, node_fs_1.writeFileSync)(credFile, JSON.stringify(keychainCreds, null, 2));
|
|
153
|
+
}
|
|
154
|
+
else if (freshness === "second" && fileCreds) {
|
|
155
|
+
// File is fresher, sync back to keychain
|
|
156
|
+
const store = (0, auth_1.getCredentialStore)();
|
|
157
|
+
store.set(getKeychainServiceName(), (0, node_os_1.userInfo)().username, JSON.stringify(fileCreds));
|
|
158
|
+
}
|
|
159
|
+
else if (!keychainCreds && !fileCreds) {
|
|
160
|
+
throw new Error("No credentials. Run: claude auth login");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
if (!(0, node_fs_1.existsSync)(credFile)) {
|
|
165
|
+
throw new Error("No credentials. Run: claude auth login");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
versionCommand: "claude --version",
|
|
170
|
+
};
|
|
171
|
+
function getCredentialPaths() {
|
|
172
|
+
return {
|
|
173
|
+
credentials: (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude", ".credentials.json"),
|
|
174
|
+
config: (0, node_path_1.join)((0, node_os_1.homedir)(), ".claude.json"),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.agents = void 0;
|
|
4
|
+
exports.detectInstalledAgents = detectInstalledAgents;
|
|
5
|
+
exports.isAgentInstalled = isAgentInstalled;
|
|
6
|
+
const node_child_process_1 = require("node:child_process");
|
|
7
|
+
const claude_1 = require("./claude");
|
|
8
|
+
exports.agents = {
|
|
9
|
+
claude: claude_1.claude,
|
|
10
|
+
};
|
|
11
|
+
function detectInstalledAgents() {
|
|
12
|
+
return Object.entries(exports.agents)
|
|
13
|
+
.filter(([_, agent]) => agent.isInstalledOnHost())
|
|
14
|
+
.map(([name]) => name);
|
|
15
|
+
}
|
|
16
|
+
function isAgentInstalled(containerName, agent) {
|
|
17
|
+
try {
|
|
18
|
+
(0, node_child_process_1.execSync)(`docker exec ${containerName} which ${agent.command}`, { stdio: "pipe" });
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCredentialPaths = void 0;
|
|
4
|
+
exports.ensureCredentials = ensureCredentials;
|
|
5
|
+
// Re-export from Claude agent for backward compatibility
|
|
6
|
+
const claude_1 = require("./agents/claude");
|
|
7
|
+
Object.defineProperty(exports, "getCredentialPaths", { enumerable: true, get: function () { return claude_1.getCredentialPaths; } });
|
|
8
|
+
function ensureCredentials() {
|
|
9
|
+
// Delegates to Claude agent's setup with current working directory
|
|
10
|
+
claude_1.claude.setup?.({ workspace: process.cwd() });
|
|
11
|
+
}
|