Spaces:
AZILS
/
Runtime error

hub / entrypoint.sh
AZILS's picture
Update entrypoint.sh
228fa29 verified
#!/usr/bin/env bash
set -euo pipefail
############################################
# Required env vars (Space 设置):
# Secrets:
# HF_TOKEN
# CODE_SERVER_PASSWORD
# BASIC_AUTH_PASSWORD
# Variables:
# CONFIG_DATASET e.g. "yourname/ubuntu"
# BASIC_AUTH_USER e.g. "gally"
# SYNC_INTERVAL_SECONDS e.g. "300"
#
# Optional:
# RESET_CLI_CONFIG=1 # if dataset has no .claude/.codex, remove local remnants to force fresh bootstrap
############################################
: "${CONFIG_DATASET:?CONFIG_DATASET is required, e.g. yourname/ubuntu}"
: "${HF_TOKEN:?HF_TOKEN secret is required}"
: "${CODE_SERVER_PASSWORD:?CODE_SERVER_PASSWORD secret is required}"
: "${BASIC_AUTH_USER:?BASIC_AUTH_USER variable is required}"
: "${BASIC_AUTH_PASSWORD:?BASIC_AUTH_PASSWORD secret is required}"
: "${SYNC_INTERVAL_SECONDS:=300}"
# Use venv python (has huggingface_hub installed)
PY="/opt/venv/bin/python"
# Canonical HOME
export HOME="/home/coder"
# npm globals (claude/codex)
export NPM_CONFIG_CACHE="${NPM_CONFIG_CACHE:-$HOME/.npm}"
export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-$HOME/.npm-global}"
export PATH="$HOME/.npm-global/bin:$PATH"
mkdir -p "$NPM_CONFIG_CACHE" "$NPM_CONFIG_PREFIX"
chown -R coder:coder "$HOME" || true
# Make interactive shells stable
cat >/etc/profile.d/00-dev-env.sh <<'EOF'
export HOME=/home/coder
export NPM_CONFIG_PREFIX="$HOME/.npm-global"
export PATH="$HOME/.npm-global/bin:$PATH"
EOF
chmod +x /etc/profile.d/00-dev-env.sh
# De-duplicate PATH injection for bash sessions
if ! grep -q 'npm-global/bin' /home/coder/.bashrc 2>/dev/null; then
cat >>/home/coder/.bashrc <<'EOF'
# >>> dev env: npm global bin (claude/codex) >>>
export HOME=/home/coder
export NPM_CONFIG_PREFIX="$HOME/.npm-global"
case ":$PATH:" in
*":$HOME/.npm-global/bin:"*) ;;
*) export PATH="$HOME/.npm-global/bin:$PATH" ;;
esac
# <<< dev env <<<
EOF
fi
chown coder:coder /home/coder/.bashrc 2>/dev/null || true
# ---- HF auth/cache MUST stay OUT of $HOME (you sync entire HOME) ----
# huggingface_hub supports HF_HOME/HF_HUB_CACHE env vars. [8](https://github.com/q09sssisiwjb/Use-vscode-chrome-terminal)
export HF_TOKEN="${HF_TOKEN}"
export HF_HOME="/tmp/hf_home"
export HF_TOKEN_PATH="/tmp/hf_home/token"
export HF_HUB_CACHE="/tmp/hf_home/hub"
export HF_ASSETS_CACHE="/tmp/hf_home/assets"
rm -rf "${HF_HOME}" 2>/dev/null || true
mkdir -p "${HF_HUB_CACHE}" "${HF_ASSETS_CACHE}"
chmod -R 777 "${HF_HOME}" 2>/dev/null || true
echo "[debug] Using PY=${PY}"
"${PY}" -c "import huggingface_hub; print('[debug] huggingface_hub=', huggingface_hub.__version__)"
# ---- Pull dataset snapshot ----
PERSIST="/persist_repo"
mkdir -p "${PERSIST}"
echo "[boot] Pull dataset snapshot -> ${PERSIST}"
"${PY}" /sync_home.py pull --repo "${CONFIG_DATASET}" --dst "${PERSIST}"
# ---- Restore dataset home -> /home/coder (NO delete) ----
# Keeps image-preinstalled dirs from being wiped.
if [ -d "${PERSIST}/home" ]; then
echo "[boot] Restore ${PERSIST}/home -> ${HOME} (NO delete)"
"${PY}" /sync_home.py rsync_in --src "${PERSIST}/home" --dst "${HOME}"
else
echo "[boot] Dataset has no 'home/' yet. Initializing..."
mkdir -p "${PERSIST}/home"
fi
# Fix ownership after restore
chown -R coder:coder "${HOME}" || true
# ---- Optional: clean bootstrap of .claude/.codex if dataset lacks them ----
# Use if you want to ensure no local remnants remain when dataset does not have these dirs.
if [ "${RESET_CLI_CONFIG:-0}" = "1" ]; then
if [ ! -d "${PERSIST}/home/.claude" ]; then rm -rf /home/coder/.claude 2>/dev/null || true; fi
if [ ! -d "${PERSIST}/home/.codex" ]; then rm -rf /home/coder/.codex 2>/dev/null || true; fi
if [ ! -f "${PERSIST}/home/.claude.json" ]; then rm -f /home/coder/.claude.json 2>/dev/null || true; fi
fi
# ---- Restore .claude/.codex only if present in dataset snapshot ----
if [ -d "${PERSIST}/home/.claude" ]; then
echo "[fix] Restore ~/.claude from dataset (authoritative)"
mkdir -p /home/coder/.claude
rsync -a --delete "${PERSIST}/home/.claude/" "/home/coder/.claude/"
chown -R coder:coder /home/coder/.claude || true
else
echo "[fix] Dataset has no .claude -> skip restore"
fi
if [ -d "${PERSIST}/home/.codex" ]; then
echo "[fix] Restore ~/.codex from dataset (authoritative)"
mkdir -p /home/coder/.codex
rsync -a --delete "${PERSIST}/home/.codex/" "/home/coder/.codex/"
chown -R coder:coder /home/coder/.codex || true
else
echo "[fix] Dataset has no .codex -> skip restore"
fi
# ---- Claude onboarding bypass flag (user-scope file) ----
# Many guides suggest setting hasCompletedOnboarding=true as a TOP-LEVEL field in ~/.claude.json. [1](https://help.aliyun.com/zh/model-studio/claude-code-coding-plan)[2](https://github.com/ding113/claude-code-hub/issues/352)[3](https://linux.do/t/topic/1416398)
# This helps avoid onboarding/login/connectivity blockers. It does not replace API auth. [1](https://help.aliyun.com/zh/model-studio/claude-code-coding-plan)[4](https://code.claude.com/docs/en/settings)
if [ ! -f /home/coder/.claude.json ]; then
cat >/home/coder/.claude.json <<'EOF'
{ "hasCompletedOnboarding": true }
EOF
else
# Ensure the key exists at top-level (simple merge: if missing, overwrite with minimal file)
if ! grep -q '"hasCompletedOnboarding"[[:space:]]*:[[:space:]]*true' /home/coder/.claude.json; then
cat >/home/coder/.claude.json <<'EOF'
{ "hasCompletedOnboarding": true }
EOF
fi
fi
chown coder:coder /home/coder/.claude.json || true
# Codex user config lives in ~/.codex/config.toml. [5](https://stackoverflow.com/questions/66496890/vs-code-nopermissions-filesystemerror-error-eacces-permission-denied)[6](https://hugging-face.cn/docs/hub/spaces-storage)
mkdir -p /home/coder/.codex
if [ ! -f /home/coder/.codex/config.toml ]; then
cat >/home/coder/.codex/config.toml <<'EOF'
# Codex user config (portable baseline)
# User-level configuration lives in ~/.codex/config.toml. [5](https://stackoverflow.com/questions/66496890/vs-code-nopermissions-filesystemerror-error-eacces-permission-denied)[6](https://hugging-face.cn/docs/hub/spaces-storage)
model_provider = "openai"
# model = "gpt-5.2"
# approval_policy = "on-request"
# sandbox_mode = "workspace-write"
EOF
fi
chown -R coder:coder /home/coder/.codex || true
# ---- Root fallback symlinks (so even processes with HOME=/root use coder configs) ----
rm -rf /root/.claude /root/.codex 2>/dev/null || true
ln -sfn /home/coder/.claude /root/.claude
ln -sfn /home/coder/.codex /root/.codex
ln -sfn /home/coder/.claude.json /root/.claude.json
# ---- Fix codex vendor binary exec bit (Codex spawns this binary) ----
CODEX_BIN="/home/coder/.npm-global/lib/node_modules/@cometix/codex/vendor/x86_64-unknown-linux-musl/codex/codex"
if [ -f "$CODEX_BIN" ]; then
echo "[fix] chmod +x codex vendor binary: $CODEX_BIN"
chmod 755 "$CODEX_BIN" || true
chmod 755 "$(dirname "$CODEX_BIN")" 2>/dev/null || true
fi
# ---- Install wrappers so claude/codex always runnable (exec-bit loss safe) ----
CLAUDE_JS="/home/coder/.npm-global/lib/node_modules/@anthropic-ai/claude-code/cli.js"
CODEX_JS="/home/coder/.npm-global/lib/node_modules/@cometix/codex/bin/codex.js"
cat >/usr/local/bin/claude <<EOF
#!/usr/bin/env bash
exec /usr/bin/node "${CLAUDE_JS}" "\$@"
EOF
chmod 755 /usr/local/bin/claude
cat >/usr/local/bin/codex <<EOF
#!/usr/bin/env bash
exec /usr/bin/node "${CODEX_JS}" "\$@"
EOF
chmod 755 /usr/local/bin/codex
# ---- Nginx basic auth ----
echo "Adding password for user ${BASIC_AUTH_USER}"
htpasswd -bc /etc/nginx/.htpasswd "${BASIC_AUTH_USER}" "${BASIC_AUTH_PASSWORD}"
if [ -f /etc/nginx/templates/nginx.conf.template ]; then
cp /etc/nginx/templates/nginx.conf.template /etc/nginx/nginx.conf
fi
# ---- code-server dirs ----
USER_DATA_DIR="/home/coder/.local/share/code-server"
EXT_DIR="/home/coder/.local/share/code-server/extensions"
mkdir -p "${USER_DATA_DIR}/User" "${EXT_DIR}"
chown -R coder:coder "${USER_DATA_DIR}" "${EXT_DIR}" || true
# 设置: create baseline ONCE, never overwrite user changes on reboot
# ---- VS Code user settings: create baseline ONCE, never overwrite user changes ----
SETTINGS_JSON="${USER_DATA_DIR}/User/settings.json"
mkdir -p "${USER_DATA_DIR}/User"
chown -R coder:coder "${USER_DATA_DIR}/User" || true
if [ ! -f "${SETTINGS_JSON}" ]; then
cat > "${SETTINGS_JSON}" <<'EOF'
{
"terminal.integrated.defaultProfile.linux": "SAFE_BASH",
"terminal.integrated.profiles.linux": {
"SAFE_BASH": { "path": "/bin/bash", "args": ["--noprofile", "--norc"] }
},
"terminal.integrated.cwd": "/home/coder",
"terminal.integrated.env.linux": {
"HOME": "/home/coder",
"NPM_CONFIG_PREFIX": "/home/coder/.npm-global",
"PATH": "/home/coder/.npm-global/bin:${env:PATH}"
},
"window.restoreWindows": "none"
}
EOF
chown coder:coder "${SETTINGS_JSON}" || true
else
echo "[boot] VS Code settings.json exists -> keep user customizations (no overwrite)"
fi
# ---- Start code-server (ignore last opened to avoid /root watcher EACCES) ----
export PASSWORD="${CODE_SERVER_PASSWORD}"
echo "[boot] Start code-server with explicit user-data-dir/extensions-dir"
su -p coder -c "export HOME=/home/coder; export PATH=/home/coder/.npm-global/bin:\$PATH; \
/usr/bin/code-server \
--bind-addr 127.0.0.1:8080 \
--auth password \
--ignore-last-opened \
--user-data-dir /home/coder/.local/share/code-server \
--extensions-dir /home/coder/.local/share/code-server/extensions \
/home/coder" &
CODE_PID=$!
# ---- Start nginx (public 7860) ----
nginx -g "daemon off;" &
NGINX_PID=$!
# ---- Sync daemon (home -> dataset) ----
"${PY}" /sync_home.py daemon \
--repo "${CONFIG_DATASET}" \
--home "${HOME}" \
--persist "${PERSIST}" \
--interval "${SYNC_INTERVAL_SECONDS}" &
SYNC_PID=$!
final_sync() {
echo "[sync] Final sync..."
"${PY}" /sync_home.py push --repo "${CONFIG_DATASET}" --home "${HOME}" --persist "${PERSIST}" || true
}
shutdown() {
echo "[signal] termination received"
final_sync
kill "${SYNC_PID}" 2>/dev/null || true
kill "${CODE_PID}" 2>/dev/null || true
kill "${NGINX_PID}" 2>/dev/null || true
exit 0
}
trap shutdown SIGTERM SIGINT
wait "${NGINX_PID}"