#!/bin/bash
set -euo pipefail
###############################################################################
# Hugging Face Spaces persistent storage
###############################################################################
# /data is ONLY available at runtime, not at image build time.
# Redirect ALL HF and Python caches here to avoid hitting the 50GB root FS cap.
# OpenCode
export HF_DATA=/data
export HOME_STATE=/data/home
export XDG_CONFIG_HOME=$HF_DATA/settings
export XDG_DATA_HOME=$HF_DATA/sessions/opencode-states
# Hugging Face
export HF_HOME=$HF_DATA/settings/huggingface
export HF_HUB_CACHE=$HF_HOME/hub
# Torch, pip, etc.
export TORCH_HOME=$HF_DATA/settings/torch
export PIP_CACHE_DIR=$HF_DATA/settings/pip
export TMPDIR=$HF_DATA/tmp
# OpenCode config directory (Git-tracked shared config)
export OPENCODE_CONFIG_DIR=$HF_DATA/workspace/opencode
mkdir -vp "$HF_HOME" "$HF_HUB_CACHE" \
"$XDG_DATA_HOME/opencode" "$TORCH_HOME" "$PIP_CACHE_DIR" "$TMPDIR" \
"$HOME_STATE/.ssh" \
"$XDG_DATA_HOME/chats" \
"$XDG_CONFIG_HOME/User" "$XDG_CONFIG_HOME/extensions" \
"$OPENCODE_CONFIG_DIR"/{agents,plugins,commands,modes}
mkdir -vp $HF_DATA/workspace/tmp
cd $HF_DATA/workspace
###############################################################################
# Disk usage guardrail
###############################################################################
DISK=$(df /data | tail -1 | awk '{print $5}' | tr -d '%')
if [ "$DISK" -gt 85 ]; then
echo "WARNING: /data at ${DISK}%. Running git gc..."
if [ -d "$OPENCODE_CONFIG_DIR/.git" ]; then
git -C "$OPENCODE_CONFIG_DIR" gc --auto 2>/dev/null || true
fi
fi
###############################################################################
# Home state persistence — shell, git, ssh survive container sleep
###############################################################################
# Seed from image on first cold-boot only (touch guarantees existence before symlink)
touch "$HOME_STATE/.bash_history" \
"$HOME_STATE/.bashrc" \
"$HOME_STATE/.gitconfig"
# Symlink ephemeral home → persistent home
ln -sf "$HOME_STATE/.gitconfig" /home/vscode/.gitconfig
ln -sf "$HOME_STATE/.bash_history" /home/vscode/.bash_history
# SSH: remove ephemeral dir first, then symlink persistently
rm -rf /home/vscode/.ssh
ln -sfn "$HOME_STATE/.ssh" /home/vscode/.ssh
chmod 700 "$HOME_STATE/.ssh" 2>/dev/null || true
# Append a single source line to the image's pristine .bashrc (idempotent guard)
if ! grep -q "source $HOME_STATE/.bashrc" /home/vscode/.bashrc; then
cat >> /home/vscode/.bashrc << EOF
# Dynamically source persistent shell customizations from /data
if [ -f "$HOME_STATE/.bashrc" ]; then
source "$HOME_STATE/.bashrc"
fi
EOF
fi
# Initialize .bashrc with history flush + sensible defaults (cold-boot seed)
if [ ! -s "$HOME_STATE/.bashrc" ]; then
cat > "$HOME_STATE/.bashrc" << 'EOF'
# Persistent shell customizations — survives container sleep.
# Shell history guardrails
export HISTFILE=/data/home/.bash_history
export HISTSIZE=50000
export HISTFILESIZE=100000
export HISTCONTROL=ignoreboth:erasedups
shopt -s histappend
# Flush after every command to survive mid-session crashes
PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND"
EOF
fi
###############################################################################
# OpenCode shared config Git repository sync
###############################################################################
if [ ! -d "$OPENCODE_CONFIG_DIR/.git" ]; then
echo "Cold boot: cloning shared OpenCode config..."
if [ -n "${GITHUB_TOKEN:-}" ]; then
GIT_URL="https://oauth2:${GITHUB_TOKEN}@github.com/suntong/opencode-cfg.git"
else
GIT_URL="https://github.com/suntong/opencode-cfg.git"
echo "WARNING: GITHUB_TOKEN not set — using public clone (repo may be private)"
fi
if ! git clone --depth 1 "$GIT_URL" "$OPENCODE_CONFIG_DIR"; then
echo "Warning: git clone failed — proceeding with empty config dirs"
else
# Seed opencode.jsonc from image on successful cold boot
if [ -f /home/vscode/.config/opencode/opencode.jsonc ]; then
cp -v /home/vscode/.config/opencode/opencode.jsonc "$OPENCODE_CONFIG_DIR/opencode.jsonc"
fi
fi
elif git -C "$OPENCODE_CONFIG_DIR" fetch --quiet --timeout 10 origin 2>/dev/null; then
echo "Warm boot: pulling shared OpenCode config..."
git -C "$OPENCODE_CONFIG_DIR" pull --autostash --ff-only || echo "Warning: git pull failed — using cached config"
else
echo "Warning: network offline — using cached shared config"
fi
###############################################################################
# Hugging Face Spaces entrypoint
#
# Services:
# oc-plugin -> 127.0.0.1:7861
# OpenCode -> 127.0.0.1:7862
# Caddy -> :7860 (foreground)
#
# Runs as user: vscode
###############################################################################
TTYD_PORT=7861
OPENCODE_PORT=7862
OPENSESAME_PORT=7866
OPENSESAME_PATH=123456
CADDY_PORT=7860
TTYD_HOST=127.0.0.1
OPENCODE_HOST=127.0.0.1
###############################################################################
# Process tracking
###############################################################################
TTYD_PID=""
OPENCODE_PID=""
OPENSESAME_PID=""
# Track Caddy PID to maintain process supervision
CADDY_PID=""
###############################################################################
# Logging helper
###############################################################################
log() {
echo "[entrypoint] $*"
}
###############################################################################
# Graceful shutdown
###############################################################################
shutdown() {
log "Shutdown signal received or process exited unexpectedly"
if [[ -n "${CADDY_PID}" ]] && kill -0 "$CADDY_PID" 2>/dev/null; then
log "Stopping Caddy (PID $CADDY_PID)"
kill -TERM "$CADDY_PID" 2>/dev/null || true
fi
if [[ -n "${TTYD_PID}" ]] && kill -0 "$TTYD_PID" 2>/dev/null; then
log "Stopping oc-plugin (PID $TTYD_PID)"
kill -TERM "$TTYD_PID" 2>/dev/null || true
fi
if [[ -n "${OPENSESAME_PID}" ]] && kill -0 "$OPENSESAME_PID" 2>/dev/null; then
log "Stopping OpenSesame (PID $OPENSESAME_PID)"
kill -TERM "$OPENSESAME_PID" 2>/dev/null || true
fi
if [[ -n "${OPENCODE_PID}" ]] && kill -0 "$OPENCODE_PID" 2>/dev/null; then
log "Stopping OpenCode (PID $OPENCODE_PID)"
kill -TERM "$OPENCODE_PID" 2>/dev/null || true
fi
log "Waiting for child processes to exit..."
wait || true
log "Shutdown complete"
}
# Trap EXIT in addition to SIGTERM and SIGINT for comprehensive cleanup
trap shutdown EXIT SIGTERM SIGINT
###############################################################################
# Wait utilities (REAL readiness checks, not just TCP open)
###############################################################################
wait_for_tcp() {
local host=$1
local port=$2
local name=$3
# Configurable timeout parameter, defaults to 60 seconds
local timeout=${4:-60}
log "Waiting for $name on $host:$port (TCP)..."
for i in $(seq 1 "$timeout"); do
# Utilize bash built-in /dev/tcp to eliminate external netcat dependency
if timeout 1 bash -c "</dev/tcp/$host/$port" >/dev/null 2>&1; then
log "$name TCP is open"
return 0
fi
sleep 1
done
log "ERROR: $name failed to open TCP port"
return 1
}
wait_for_http() {
local url=$1
local name=$2
# Configurable timeout parameter, defaults to 120 seconds
local timeout=${3:-120}
log "Waiting for $name HTTP readiness: $url"
for i in $(seq 1 "$timeout"); do
if curl -fsS "$url" >/dev/null 2>&1; then
log "$name is ready"
return 0
fi
sleep 1
done
log "ERROR: $name did not become ready"
#return 1 # disable to let go
}
###############################################################################
# Start oc-plugin
###############################################################################
start_oc-plugin() {
log "Starting oc-plugin..."
# Launching bash securely within the oc-plugin context as a login shell
oc-plugin \
-p "$TTYD_PORT" \
-i "$TTYD_HOST" \
-W \
bash -l &
TTYD_PID=$!
log "oc-plugin PID: $TTYD_PID"
}
###############################################################################
# Start OpenSesame
###############################################################################
start_opensesame() {
OpenSesame -fx $OPENSESAME_PATH -path $HF_DATA -port :$OPENSESAME_PORT &
OPENSESAME_PID=$!
log "OpenSesame PID: $OPENSESAME_PID"
}
###############################################################################
# Start OpenCode
###############################################################################
start_opencode() {
log "Starting OpenCode..."
cd $HF_DATA/workspace/tmp
# Start OpenCode & expose HTTP on 7862 internally.
opencode web --port "$OPENCODE_PORT" --hostname "$OPENCODE_HOST" &
OPENCODE_PID=$!
log "OpenCode PID: $OPENCODE_PID"
}
###############################################################################
# Main startup sequence
###############################################################################
log "Boot sequence starting..."
start_oc-plugin
wait_for_tcp "$TTYD_HOST" "$TTYD_PORT" "oc-plugin"
# Launch opencode (it serves its web UI on port 7862)
start_opencode
wait_for_tcp "$OPENCODE_HOST" "$OPENCODE_PORT" "OpenCode (TCP)"
# Optional stronger check (preferred if OpenCode exposes /health or /)
# Extended timeout to 25min to accommodate heavy IDE startup
wait_for_http "http://127.0.0.1:7862/" "OpenCode HTTP" 1500
log "All backends are ready"
###############################################################################
# Launch Caddy (foreground = HF requirement)
###############################################################################
log "Starting Caddy..."
# Run Caddy in background to maintain shell as PID 1 supervisor
caddy run \
--config /etc/caddy/Caddyfile \
--adapter caddyfile &
CADDY_PID=$!
log "Caddy PID: $CADDY_PID"
###############################################################################
# Process Supervision
###############################################################################
# Block until any single child process exits, then exit shell to trigger restart
wait -n
log "A supervised process has exited. Initiating container termination."
exit 1
Created
July 4, 2026 00:43
-
-
Save suntong/1d8287ead9bc6460ca91aeafdcabdf5f to your computer and use it in GitHub Desktop.
File: entrypoint.sh
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here's the updated directory layout matching the code in
entrypoint.sh:Key corrections:
.bashrc(not.bashrc_custom),opencode.jsonc(notopencode.json),sessions/opencode-states/opencode/matches the$XDG_DATA_HOME/opencodepath, and HF/torch/pip caches reside under/data/settings/.Consolidated Task Specification
I need to redesign
entrypoint.sh(code-v0) for my OpenCode deployment on Hugging Face Spaces to fit within a strict 50GB persistent volume limit, without losing critical AI state or breaking pre-installed addon skills.1. Storage Strategy & Constraints
/datavolume and keep only essential state there.node_modulesElimination: I must remove all logic that preservesnode_modules/under/data/workspace/opencode/. It is reproducible frompackage.jsonand acceptable to lose on cold start./data/settings/User/or/data/settings/extensions/.HF_HOME,PIP_CACHE_DIR,TORCH_HOME, or any npm/TMP*vars. I will rely entirely on image defaults for pre-installed tools, and accept that any new in-container package installs are volatile (lost on cold start). I will also not pre-create/tmp/sessions/*scratch dirs.2. Persistence Requirements (The "Must-Haves")
Only the following will live in
/data:/data/workspace/<user-projects>/(Priority: High, ~35-40GB)./data/workspace/opencode/(Containing agents, plugins, commands, modes,opencode.jsonc— the Git-tracked config repo)./data/sessions/memory.json(Long-term semantic memory; ~1-10MB)./data/sessions/chats/(Conversation history; ~100MB-1GB)./data/sessions/opencode/(Workspace indexes, agent states; ~500MB-2GB).3. Environment Variable & Naming Conventions
OPENCODE_WORKSPACE_DIR(parent of all workspaces) andXDG_DATA_HOME(cascade root for OpenCode's data subtree). Since the HF Space runs only OpenCode, settingXDG_DATA_HOME=/data/sessionsis safe and scoped — no foreign process will pollute it.OPENCODE_→CFGOC_):CFGOC_SHARED_CONFIG_DIR(/data/workspace/opencode),CFGOC_CONFIG_REPO_URL(optional clone source),CFGOC_CONFIG_AUTO_PULL(trueto pull on boot),CFGOC_PACKAGED_SKILLS_DIR(/home/vscode/.config/opencode/skills).OPENCODE_WORKSPACE_DIR,XDG_DATA_HOME, and theCFGOC_*helpers). Let OpenCode derive sub-paths automatically — e.g.,OPENCODE_DATA_DIRresolves internally to$XDG_DATA_HOME/opencode→/data/sessions/opencode;OPENCODE_AGENTS_DIR,OPENCODE_SKILLS_DIR,OPENCODE_COMMANDS_DIR,OPENCODE_MODES_DIRresolve against$CFGOC_SHARED_CONFIG_DIR. Use comments — not duplicate vars — to document.4. Configuration & Skill Integrity
/home/vscode/.config/opencode/skills/(verified — there is nodefaults/skills/sub-path). OpenCode discovers them via its global skills lookup. The script must not copy them into/data; it must let OpenCode use them in-place from the image home. This respects the image's curated lifecycle and keeps/datalean./data/workspace/opencode/, cloned fromCFGOC_CONFIG_REPO_URLif set, otherwise scaffolded empty. Only this directory is persistent and Git-tracked.$CFGOC_SHARED_CONFIG_DIR/.gitis absent): clone fromCFGOC_CONFIG_REPO_URLif provided; otherwise create an empty scaffold. If clone fails, fall back to the empty scaffold rather than silently corrupting state.opencode.jsoncinto Layer B — let OpenCode fall through to the image-level/home/vscode/.config/opencode/opencode.jsonc(verified present, already wired for Agnes/NIM/openai-compatible providers, LSP,0.0.0.0:7860). Copying would freeze the image default and block clean upgrades.CFGOC_CONFIG_AUTO_PULL=trueandCFGOC_CONFIG_REPO_URLis set, rungit pull origin main(tolerate network failures gracefully).mkdir -p/data/sessions/opencodeand/data/sessions/chats/, and initialize/data/sessions/memory.jsonwith{"memory": {}, "version": "1.0"}if absent — since OpenCode may not auto-create these on cold boot.5. Target Directory Layout
workspace/opencode/→ Shared config repo (Layer B).workspace/<projects>/→ User code.sessions/opencode/→ Runtime state (viaXDG_DATA_HOMEcascade).sessions/chats/→ Histories.sessions/memory.json→ Memory./home/vscode/.config/opencode/skills/→ Packaged "superpowers" skills (Layer A)./home/vscode/.config/opencode/opencode.jsonc→ Image default config.6. Permissions & Handoff
chown -R vscode:vscode /data.exec su-exec vscode node /home/vscode/opencode/server/index.js.