#!/usr/bin/env bash #ddev-generated set -euo pipefail # Lazy installer for claude-swap (cswap): a multi-account manager for Claude Code. # It is NOT installed during "ddev start" - the first "ddev claude-swap ..." call # (or "ddev claude --account ...") runs this script, which installs uv and then # "claude-swap" into the same project-local, persisted profile that the Claude # Code CLI uses. Pass --upgrade to update an existing install. # # All state lives under .ddev/claude-code so it survives container rebuilds # without being committed to the project repository, exactly like install.sh. PERSIST_BASE="${DDEV_APPROOT}/.ddev/claude-code" PERSIST_HOME="${PERSIST_BASE}/home" # replaces $HOME inside the container PERSIST_CONFIG_HOME="${PERSIST_BASE}/config" # XDG_CONFIG_HOME PERSIST_DATA_HOME="${PERSIST_BASE}/data" # XDG_DATA_HOME PERSIST_STATE_HOME="${PERSIST_BASE}/state" # XDG_STATE_HOME PERSIST_BIN_DIR="${PERSIST_HOME}/.local/bin" # shared with the claude launcher UV_BIN="${PERSIST_BIN_DIR}/uv" CSWAP_BIN="${PERSIST_BIN_DIR}/cswap" mkdir -p \ "${PERSIST_HOME}" \ "${PERSIST_CONFIG_HOME}" \ "${PERSIST_DATA_HOME}" \ "${PERSIST_STATE_HOME}" \ "${PERSIST_BIN_DIR}" # Point HOME / XDG paths at the persisted profile before any install or health # check so cswap's own data (Linux: ${XDG_DATA_HOME}/claude-swap) is per-project # and survives restarts with no extra configuration. export HOME="${PERSIST_HOME}" export XDG_CONFIG_HOME="${PERSIST_CONFIG_HOME}" export XDG_DATA_HOME="${PERSIST_DATA_HOME}" export XDG_STATE_HOME="${PERSIST_STATE_HOME}" export PATH="${PERSIST_BIN_DIR}:${PATH}" # Keep every artifact uv creates inside the persisted tree. export UV_INSTALL_DIR="${PERSIST_BIN_DIR}" # uv / uvx binaries export UV_TOOL_BIN_DIR="${PERSIST_BIN_DIR}" # tool entry points (cswap) export UV_TOOL_DIR="${XDG_DATA_HOME}/uv/tools" # tool virtualenvs export UV_PYTHON_INSTALL_DIR="${XDG_DATA_HOME}/uv/python" # uv-managed CPython export UV_CACHE_DIR="${XDG_DATA_HOME}/uv/cache" # download / build cache # Remove an unrunnable persisted uv (e.g. wrong arch after a host OS change). if [ -x "${UV_BIN}" ] && ! "${UV_BIN}" --version >/dev/null 2>&1; then echo "Persisted uv binary is not runnable, reinstalling ..." rm -f "${UV_BIN}" "${PERSIST_BIN_DIR}/uvx" fi # Install uv when missing. uv fetches its own CPython 3.12 below, so the DDEV web # image's system Python (3.11) is never relied upon. Do not let the installer # edit shell rc files in the persisted HOME - PATH is set by the launchers. if [ ! -x "${UV_BIN}" ]; then echo "Installing uv ..." curl -fsSL https://astral.sh/uv/install.sh | env INSTALLER_NO_MODIFY_PATH=1 sh fi if [ ! -x "${UV_BIN}" ]; then echo "uv installation completed but no executable was found at ${UV_BIN}." >&2 echo "If outbound access to astral.sh is blocked this step cannot succeed." >&2 exit 1 fi if [ "${1:-}" = "--upgrade" ]; then echo "Upgrading claude-swap ..." "${UV_BIN}" tool upgrade claude-swap echo "claude-swap version: $("${CSWAP_BIN}" --version)" exit 0 fi # Install claude-swap when missing or unrunnable. if [ ! -x "${CSWAP_BIN}" ] || ! "${CSWAP_BIN}" --version >/dev/null 2>&1; then echo "Installing claude-swap ..." "${UV_BIN}" tool install --force --python 3.12 claude-swap fi if [ ! -x "${CSWAP_BIN}" ] || ! "${CSWAP_BIN}" --version >/dev/null 2>&1; then echo "claude-swap installation completed but ${CSWAP_BIN} is not runnable." >&2 echo "First install needs outbound access to astral.sh, pypi.org," >&2 echo "files.pythonhosted.org and github.com (uv-managed CPython)." >&2 exit 1 fi echo "claude-swap version: $("${CSWAP_BIN}" --version)"