Skip to content

Instantly share code, notes, and snippets.

@suntong
Created July 4, 2026 00:43
Show Gist options
  • Select an option

  • Save suntong/1d8287ead9bc6460ca91aeafdcabdf5f to your computer and use it in GitHub Desktop.

Select an option

Save suntong/1d8287ead9bc6460ca91aeafdcabdf5f to your computer and use it in GitHub Desktop.
File: entrypoint.sh
#!/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

@suntong

suntong commented Jul 4, 2026

Copy link
Copy Markdown
Author

Here's the updated directory layout matching the code in entrypoint.sh:

/data/
├── home/                          # Persistent user state (HOME_STATE)
│   ├── .bash_history              # Persistent terminal command history
│   ├── .bashrc                    # Persistent shell customizations (aliases, env variables)
│   ├── .gitconfig                 # Global Git credentials and configurations
│   └── .ssh/                      # Secure SSH keys and known_hosts file
├── workspace/                     # Default root workspace directory
│   ├── opencode/                  # Shared global configurations (Git Repository from opencode-cfg)
│   │   ├── agents/                # Global custom agent markdown files (.md)
│   │   ├── plugins/               # Shared plugins
│   │   ├── commands/              # Shared custom workspace commands
│   │   ├── modes/                 # Custom operational modes
│   │   └── opencode.jsonc         # Master workspace configuration (seeded from /home/vscode/.config/opencode/)
│   ├── tmp/                       # Temporary workspace (OpenCode serves from here)
│   └── <project repos>            # Independent user project directories
├── settings/                      # XDG_CONFIG_HOME — consolidated settings & configs
│   ├── opencode/                  # (overlaps with opencode config area)
│   ├── huggingface/               # HF_HOME — HuggingFace cache & hub
│   │   └── hub/                   # HF_HUB_CACHE
│   ├── torch/                     # TORCH_HOME
│   ├── pip/                       # PIP_CACHE_DIR
│   ├── User/                      # VS Code / OpenCode settings.json and keybindings
│   └── extensions/                # Installed extensions
├── sessions/                      # XDG_DATA_HOME — AI run-state and logs
│   ├── opencode-states/           # Added subpath: opencode
│   │   └── opencode/
│   ├── chats/                     # AI chat histories and session logs
│   └── memory.json                # Long-term recall store and agent memory
└── tmp/                           # TMPDIR — general temp files

Key corrections: .bashrc (not .bashrc_custom), opencode.jsonc (not opencode.json), sessions/opencode-states/opencode/ matches the $XDG_DATA_HOME/opencode path, 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

  • The 50GB Limit: I must strictly prioritize the persistent /data volume and keep only essential state there.
  • node_modules Elimination: I must remove all logic that preserves node_modules/ under /data/workspace/opencode/. It is reproducible from package.json and acceptable to lose on cold start.
  • VS Code Exclusion: Since this Space runs only OpenCode (no VS Code), I must not create or preserve /data/settings/User/ or /data/settings/extensions/.
  • No Cache Redirection: I will not explicitly export 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:

  • User Workspaces: /data/workspace/<user-projects>/ (Priority: High, ~35-40GB).
  • Shared Config: /data/workspace/opencode/ (Containing agents, plugins, commands, modes, opencode.jsonc — the Git-tracked config repo).
  • AI State (all persisted):
    • /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

  • Official Vars (kept as-is): OPENCODE_WORKSPACE_DIR (parent of all workspaces) and XDG_DATA_HOME (cascade root for OpenCode's data subtree). Since the HF Space runs only OpenCode, setting XDG_DATA_HOME=/data/sessions is safe and scoped — no foreign process will pollute it.
  • Custom Vars (rename OPENCODE_CFGOC_): CFGOC_SHARED_CONFIG_DIR (/data/workspace/opencode), CFGOC_CONFIG_REPO_URL (optional clone source), CFGOC_CONFIG_AUTO_PULL (true to pull on boot), CFGOC_PACKAGED_SKILLS_DIR (/home/vscode/.config/opencode/skills).
  • Cascade Rule (Minimum Viable Configuration): Set only the essential parent vars (OPENCODE_WORKSPACE_DIR, XDG_DATA_HOME, and the CFGOC_* helpers). Let OpenCode derive sub-paths automatically — e.g., OPENCODE_DATA_DIR resolves internally to $XDG_DATA_HOME/opencode/data/sessions/opencode; OPENCODE_AGENTS_DIR, OPENCODE_SKILLS_DIR, OPENCODE_COMMANDS_DIR, OPENCODE_MODES_DIR resolve against $CFGOC_SHARED_CONFIG_DIR. Use comments — not duplicate vars — to document.

4. Configuration & Skill Integrity

  • Two-Layer Provenance (no cross-copying):
    • Layer A — Image Packaged Skills: The pre-installed "superpowers" skill set (14 skills: brainstorming, dispatching-parallel-agents, executing-plans, finishing-a-development-branch, receiving/requesting-code-review, subagent-driven-development, systematic-debugging, test-driven-development, using-git-worktrees, using-superpowers, verification-before-completion, writing-plans, writing-skills) lives at /home/vscode/.config/opencode/skills/ (verified — there is no defaults/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 /data lean.
    • Layer B — User Shared Config: /data/workspace/opencode/, cloned from CFGOC_CONFIG_REPO_URL if set, otherwise scaffolded empty. Only this directory is persistent and Git-tracked.
  • Bootstrapping Logic:
    • On first boot (when $CFGOC_SHARED_CONFIG_DIR/.git is absent): clone from CFGOC_CONFIG_REPO_URL if provided; otherwise create an empty scaffold. If clone fails, fall back to the empty scaffold rather than silently corrupting state.
    • Do not copy opencode.jsonc into 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.
    • On subsequent boots: if CFGOC_CONFIG_AUTO_PULL=true and CFGOC_CONFIG_REPO_URL is set, run git pull origin main (tolerate network failures gracefully).
  • AI State Initialization: The script must mkdir -p /data/sessions/opencode and /data/sessions/chats/, and initialize /data/sessions/memory.json with {"memory": {}, "version": "1.0"} if absent — since OpenCode may not auto-create these on cold boot.

5. Target Directory Layout

  • Persistent (/data):
    • workspace/opencode/ → Shared config repo (Layer B).
    • workspace/<projects>/ → User code.
    • sessions/opencode/ → Runtime state (via XDG_DATA_HOME cascade).
    • sessions/chats/ → Histories.
    • sessions/memory.json → Memory.
  • Image-owned (read-only, not persisted by script):
    • /home/vscode/.config/opencode/skills/ → Packaged "superpowers" skills (Layer A).
    • /home/vscode/.config/opencode/opencode.jsonc → Image default config.
  • Ephemeral: Untouched — image defaults drive all caches and runtime disposables.

6. Permissions & Handoff

  • Normalize ownership: chown -R vscode:vscode /data.
  • Transition to application layer: exec su-exec vscode node /home/vscode/opencode/server/index.js.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment