#!/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 </usr/local/bin/codex < "${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}"