|
#!/usr/bin/env bash |
|
# |
|
# Creates a Vercel Sandbox with Claude and GitHub CLI installed |
|
# Clones your current project and creates a new branch in the sandbox |
|
# Runs the command in the sandbox and pushes the branch back to the original repository |
|
|
|
# Usage: |
|
# ./sandbox.sh [--copy <file>...] <command> [args...] |
|
# |
|
|
|
set -e |
|
|
|
# Parse --copy flags |
|
FILES_TO_COPY=() |
|
while [[ $# -gt 0 ]]; do |
|
case $1 in |
|
--copy) |
|
FILES_TO_COPY+=("$2") |
|
shift 2 |
|
;; |
|
*) |
|
break |
|
;; |
|
esac |
|
done |
|
|
|
if [ $# -eq 0 ]; then |
|
echo "Usage: sandbox.sh [--copy <file>...] <command> [args...]" |
|
echo "Example: |
|
sandbox.sh --copy spec.md bash -c ' |
|
claude -p \"Study spec.md and do the thing\" |
|
'" |
|
exit 1 |
|
fi |
|
|
|
# Check for AI Gateway API key in keychain |
|
# This is better than passing it in as an environment variable because it's not logged to the console. |
|
# To set it, run: |
|
# security add-generic-password -a "$USER" -s "AI_GATEWAY_API_KEY" -w |
|
# |
|
ANTHROPIC_TOKEN=$(security find-generic-password -a "$USER" -s "AI_GATEWAY_API_KEY" -w 2>/dev/null || true) |
|
if [ -z "$ANTHROPIC_TOKEN" ]; then |
|
echo "Error: No AI Gateway API key found in keychain." |
|
echo "" |
|
echo "To add your token, run:" |
|
echo " security add-generic-password -a \"\$USER\" -s \"AI_GATEWAY_API_KEY\" -w" |
|
echo "" |
|
echo "Or get a token from: https://console.anthropic.com/settings/keys" |
|
exit 1 |
|
fi |
|
|
|
# Get git info from local |
|
REPO_URL=$(git config --get remote.origin.url) |
|
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || echo "main") |
|
GIT_USER_NAME=$(git config --get user.name || echo "Sandbox User") |
|
GIT_USER_EMAIL=$(git config --get user.email || echo "sandbox@example.com") |
|
|
|
# Extract owner/repo from URL |
|
REPO_PATH=$(echo "$REPO_URL" | sed -E 's|.*github\.com[:/](.+)(\.git)?$|\1|' | sed 's/\.git$//') |
|
|
|
echo "┌──────────────────────────────────────────────────" |
|
echo "│ Repository: $REPO_PATH" |
|
echo "│ Branch: $BRANCH" |
|
if [ ${#FILES_TO_COPY[@]} -gt 0 ]; then |
|
echo "│ Copy: ${FILES_TO_COPY[*]}" |
|
fi |
|
echo "│ Command: $*" |
|
echo "└──────────────────────────────────────────────────" |
|
echo |
|
|
|
# Create sandbox |
|
printf "Creating sandbox... " |
|
SANDBOX_ID=$(sandbox create --timeout 10m 2>&1 | grep -o 'sbx_[a-zA-Z0-9]*') |
|
echo "✓ $SANDBOX_ID" |
|
|
|
# Install Claude Code CLI (silent) |
|
printf "Installing Claude CLI... " |
|
sandbox exec "$SANDBOX_ID" npm install -g @anthropic-ai/claude-code @antfu/ni > /dev/null 2>&1 |
|
echo "✓" |
|
|
|
# Create Claude config with token from local keychain (using sandbox copy to avoid logging) |
|
CLAUDE_CONFIG_TMP=$(mktemp) |
|
cat > "$CLAUDE_CONFIG_TMP" << EOF |
|
{"env":{"ANTHROPIC_BASE_URL":"https://ai-gateway.vercel.sh","ANTHROPIC_API_KEY":"$ANTHROPIC_TOKEN"}} |
|
EOF |
|
sandbox exec "$SANDBOX_ID" mkdir -p .claude > /dev/null 2>&1 |
|
sandbox copy "$CLAUDE_CONFIG_TMP" "$SANDBOX_ID":.claude/settings.json > /dev/null 2>&1 |
|
rm "$CLAUDE_CONFIG_TMP" |
|
|
|
# Copy Claude skills and commands from local |
|
printf "Copying Claude skills... " |
|
if [ -d "$HOME/.claude/skills" ]; then |
|
sandbox exec "$SANDBOX_ID" mkdir -p .claude/skills > /dev/null 2>&1 |
|
for skill in "$HOME/.claude/skills"/*; do |
|
[ -e "$skill" ] && sandbox copy "$skill" "$SANDBOX_ID":.claude/skills/ > /dev/null 2>&1 |
|
done |
|
fi |
|
if [ -d "$HOME/.claude/commands" ]; then |
|
sandbox exec "$SANDBOX_ID" mkdir -p .claude/commands > /dev/null 2>&1 |
|
for cmd in "$HOME/.claude/commands"/*; do |
|
[ -e "$cmd" ] && sandbox copy "$cmd" "$SANDBOX_ID":.claude/commands/ > /dev/null 2>&1 |
|
done |
|
fi |
|
echo "✓" |
|
|
|
# Install GitHub CLI (silent) |
|
printf "Installing GitHub CLI... " |
|
sandbox exec "$SANDBOX_ID" -- bash -c "curl -fsSL https://github.com/cli/cli/releases/download/v2.67.0/gh_2.67.0_linux_amd64.tar.gz | tar xz && sudo mv gh_2.67.0_linux_amd64/bin/gh /usr/local/bin/ && rm -rf gh_2.67.0_linux_amd64" > /dev/null 2>&1 |
|
echo "✓" |
|
|
|
# Interactive GitHub login |
|
echo "" |
|
echo "GitHub authentication required:" |
|
sandbox exec --interactive --tty "$SANDBOX_ID" gh auth login |
|
echo "" |
|
|
|
# Clone repository (silent) |
|
printf "Cloning repository... " |
|
sandbox exec "$SANDBOX_ID" -- bash -c "gh repo clone '$REPO_PATH' /tmp/repo -- --branch '$BRANCH' --single-branch && shopt -s dotglob && mv /tmp/repo/* . && rm -rf /tmp/repo" > /dev/null 2>&1 |
|
echo "✓" |
|
|
|
# Configure git identity (silent) |
|
sandbox exec "$SANDBOX_ID" git config user.email "$GIT_USER_EMAIL" > /dev/null 2>&1 |
|
sandbox exec "$SANDBOX_ID" git config user.name "$GIT_USER_NAME" > /dev/null 2>&1 |
|
sandbox exec "$SANDBOX_ID" git config push.autoSetupRemote true > /dev/null 2>&1 |
|
|
|
# Install project dependencies |
|
printf "Installing dependencies... " |
|
sandbox exec "$SANDBOX_ID" ni install --silent > /dev/null 2>&1 |
|
echo "✓" |
|
|
|
# Create branch (silent) |
|
printf "Creating branch... " |
|
sandbox exec "$SANDBOX_ID" git checkout -b "$SANDBOX_ID" > /dev/null 2>&1 |
|
echo "✓" |
|
|
|
# Copy specified files from working tree |
|
if [ ${#FILES_TO_COPY[@]} -gt 0 ]; then |
|
printf "Copying files... " |
|
for file in "${FILES_TO_COPY[@]}"; do |
|
if [ -e "$file" ]; then |
|
# Create parent directory if needed |
|
dir=$(dirname "$file") |
|
if [ "$dir" != "." ]; then |
|
sandbox exec "$SANDBOX_ID" mkdir -p "$dir" > /dev/null 2>&1 |
|
fi |
|
sandbox copy "$file" "$SANDBOX_ID":"$file" > /dev/null 2>&1 |
|
else |
|
echo "" |
|
echo "Warning: File not found: $file" |
|
fi |
|
done |
|
echo "✓" |
|
fi |
|
|
|
# Run the command |
|
echo "" |
|
echo "┌──────────────────────────────────────────────────" |
|
echo "│ Running: $*" |
|
echo "└──────────────────────────────────────────────────" |
|
echo "" |
|
|
|
sandbox exec "$SANDBOX_ID" "$@" > /dev/null 2>&1 |
|
EXIT_CODE=$? |
|
|
|
echo "" |
|
|
|
# Push the branch (silent) |
|
printf "Pushing branch... " |
|
sandbox exec "$SANDBOX_ID" git push > /dev/null 2>&1 |
|
echo "✓" |
|
|
|
echo "" |
|
echo "┌──────────────────────────────────────────────────" |
|
echo "│ ✓ Complete" |
|
echo "│ Branch: $SANDBOX_ID" |
|
echo "│ PR: https://github.com/$REPO_PATH/pull/new/$SANDBOX_ID" |
|
echo "└──────────────────────────────────────────────────" |
|
|
|
exit $EXIT_CODE |
For me, I found it useful to add the --scope and --timeout flags that get passed to the sandbox cli. I threw it in this fork if you're interested.