diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index aebfdcc..8b783b9 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -10,7 +10,10 @@ RUN curl -L -o /usr/local/bin/pixi -fsSL --compressed "https://github.com/prefix USER vscode WORKDIR /home/vscode -RUN echo 'eval "$(pixi completion -s bash)"' >> /home/vscode/.bashrc +RUN echo 'eval "$(pixi completion -s bash)"' >> /home/vscode/.bashrc \ + && echo 'export PATH="$HOME/.pixi/bin:$PATH"' >> /home/vscode/.profile \ + && echo '# Workaround: pixi trampoline fails for bash scripts, so add env bin directly' >> /home/vscode/.profile \ + && echo '[ -d "$HOME/.pixi/envs/claude-shim/bin" ] && export PATH="$HOME/.pixi/envs/claude-shim/bin:$PATH"' >> /home/vscode/.profile # Create .ssh directory with proper permissions for SSH config mounts RUN mkdir -p /home/vscode/.ssh && chmod 700 /home/vscode/.ssh diff --git a/.devcontainer/claude-code/devcontainer-feature.json b/.devcontainer/claude-code/devcontainer-feature.json index a1f5f44..06b9ff5 100644 --- a/.devcontainer/claude-code/devcontainer-feature.json +++ b/.devcontainer/claude-code/devcontainer-feature.json @@ -1,8 +1,8 @@ { "name": "Claude Code CLI", "id": "claude-code", - "version": "0.1.0", - "description": "Installs Claude Code CLI globally and mounts configuration directories", + "version": "0.2.0", + "description": "Installs Claude Code CLI via pixi with persistent configuration", "options": {}, "documentationURL": "https://github.com/anthropics/devcontainer-features", "licenseURL": "https://github.com/anthropics/devcontainer-features/blob/main/LICENSE", @@ -16,16 +16,7 @@ "containerEnv": { "CLAUDE_CONFIG_DIR": "/home/vscode/.claude" }, - "dependsOn": { - "ghcr.io/devcontainers/features/node": {} - }, "mounts": [ - "source=${localEnv:HOME}/.claude/CLAUDE.md,target=/home/vscode/.claude/CLAUDE.md,type=bind,ro", - "source=${localEnv:HOME}/.claude/settings.json,target=/home/vscode/.claude/settings.json,type=bind,ro", - "source=${localEnv:HOME}/.claude/.credentials.json,target=/home/vscode/.claude/.credentials.json,type=bind", - "source=${localEnv:HOME}/.claude/.claude.json,target=/home/vscode/.claude/.claude.json,type=bind", - "source=${localEnv:HOME}/.claude/agents,target=/home/vscode/.claude/agents,type=bind,ro", - "source=${localEnv:HOME}/.claude/commands,target=/home/vscode/.claude/commands,type=bind,ro", - "source=${localEnv:HOME}/.claude/hooks,target=/home/vscode/.claude/hooks,type=bind,ro" + "source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind" ] } diff --git a/.devcontainer/claude-code/init-host.sh b/.devcontainer/claude-code/init-host.sh new file mode 100755 index 0000000..7e123bf --- /dev/null +++ b/.devcontainer/claude-code/init-host.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# Initialize Claude Code host directory for devcontainer bind mount +# This script runs on the HOST before the container is created. +# mkdir -p is idempotent - it only creates if missing, won't clobber existing. + +mkdir -p "$HOME/.claude" diff --git a/.devcontainer/claude-code/install.sh b/.devcontainer/claude-code/install.sh index b85f96a..020018f 100755 --- a/.devcontainer/claude-code/install.sh +++ b/.devcontainer/claude-code/install.sh @@ -2,116 +2,165 @@ set -eu # Claude Code CLI Local Feature Install Script -# Based on: https://github.com/anthropics/devcontainer-features/pull/25 -# Combines CLI installation with configuration directory setup +# Installs Claude Code via pixi and sets up configuration directories + +# Global variables set by resolve_target_home +TARGET_USER="" +TARGET_HOME="" + +# Function to resolve target user and home directory with validation +# Sets TARGET_USER and TARGET_HOME global variables +resolve_target_home() { + TARGET_USER="${_REMOTE_USER:-vscode}" + TARGET_HOME="${_REMOTE_USER_HOME:-}" + + # If _REMOTE_USER_HOME is not set, try to infer from current user or /home/ + if [ -z "${TARGET_HOME}" ]; then + if [ "$(id -un 2>/dev/null)" = "${TARGET_USER}" ] && [ -n "${HOME:-}" ]; then + TARGET_HOME="${HOME}" + elif [ -d "/home/${TARGET_USER}" ]; then + TARGET_HOME="/home/${TARGET_USER}" + fi + fi -# Function to install Claude Code CLI -install_claude_code() { - echo "Installing Claude Code CLI globally..." + # If TARGET_HOME is set but doesn't exist, try fallbacks + if [ -n "${TARGET_HOME}" ] && [ ! -d "${TARGET_HOME}" ]; then + if [ -n "${HOME:-}" ] && [ -d "$HOME" ]; then + echo "Warning: TARGET_HOME '${TARGET_HOME}' does not exist, falling back to \$HOME: $HOME" >&2 + TARGET_HOME="$HOME" + elif [ -d "/home/${TARGET_USER}" ]; then + echo "Warning: TARGET_HOME '${TARGET_HOME}' does not exist, falling back to /home/${TARGET_USER}" >&2 + TARGET_HOME="/home/${TARGET_USER}" + fi + fi - # Verify Node.js and npm are available (should be installed via dependsOn) - if ! command -v node >/dev/null || ! command -v npm >/dev/null; then - cat <&2 + echo "Checked _REMOTE_USER_HOME ('${_REMOTE_USER_HOME:-}'), \$HOME ('${HOME:-}'), and /home/${TARGET_USER}." >&2 + exit 1 + fi +} -ERROR: Node.js and npm are required but not found! +# Function to install pixi if not found +install_pixi() { + echo "Installing pixi..." -This should not happen as the Node.js feature is declared in 'dependsOn'. + # Detect architecture + case "$(uname -m)" in + x86_64|amd64) ARCH="x86_64" ;; + aarch64|arm64) ARCH="aarch64" ;; + *) echo "Unsupported architecture: $(uname -m)" >&2; exit 1 ;; + esac -Please check: -1. The devcontainer feature specification is correct -2. The Node.js feature (ghcr.io/devcontainers/features/node) is available -3. Your devcontainer build logs for errors + # Download and install pixi + curl -fsSL "https://github.com/prefix-dev/pixi/releases/latest/download/pixi-${ARCH}-unknown-linux-musl" -o /usr/local/bin/pixi + chmod +x /usr/local/bin/pixi -EOF - exit 1 + echo "pixi installed successfully" + pixi --version +} + +# Function to install Claude Code CLI via pixi +install_claude_code() { + echo "Installing Claude Code CLI via pixi..." + + # Install pixi if not available + if ! command -v pixi >/dev/null; then + install_pixi fi - # Install with npm - npm install -g @anthropic-ai/claude-code + # Resolve target user and home (sets TARGET_USER and TARGET_HOME) + resolve_target_home - # Verify installation - if command -v claude >/dev/null; then + # Install with pixi global from blooop channel + # Run as target user so it installs to their home directory + if [ "$(id -u)" -eq 0 ] && [ "$TARGET_USER" != "root" ]; then + su - "$TARGET_USER" -c "pixi global install --channel https://prefix.dev/blooop claude-shim" + else + pixi global install --channel https://prefix.dev/blooop claude-shim + fi + + # Add pixi bin path to user's profile if not already there + local profile="$TARGET_HOME/.profile" + local pixi_path_line='export PATH="$HOME/.pixi/bin:$PATH"' + if [ -f "$profile" ] && ! grep -q '\.pixi/bin' "$profile"; then + echo "$pixi_path_line" >> "$profile" + elif [ ! -f "$profile" ]; then + echo "$pixi_path_line" > "$profile" + chown "$TARGET_USER:$TARGET_USER" "$profile" 2>/dev/null || true + fi + + # Workaround: pixi trampoline fails for bash scripts, so add env bin directly + # This conditionally adds the path only if the env exists + local env_path_line='[ -d "$HOME/.pixi/envs/claude-shim/bin" ] && export PATH="$HOME/.pixi/envs/claude-shim/bin:$PATH"' + if [ -f "$profile" ] && ! grep -q 'pixi/envs/claude-shim' "$profile"; then + echo "# Workaround: pixi trampoline fails for bash scripts" >> "$profile" + echo "$env_path_line" >> "$profile" + fi + + # Verify installation by checking the trampoline exists (don't run it - that triggers download) + local pixi_bin_path="$TARGET_HOME/.pixi/bin" + local claude_bin="$pixi_bin_path/claude" + if [ -x "$claude_bin" ]; then echo "Claude Code CLI installed successfully!" - claude --version + echo "(Claude binary will be downloaded on first run)" return 0 else - echo "ERROR: Claude Code CLI installation failed!" + echo "ERROR: Claude Code CLI installation failed! Binary not found at $claude_bin" return 1 fi } # Function to create Claude configuration directories -# These directories will be mounted from the host, but we create them -# in the container to ensure they exist and have proper permissions create_claude_directories() { echo "Creating Claude configuration directories..." - # Determine the target user's home directory - # $_REMOTE_USER is set by devcontainer, fallback to 'vscode' - local target_user="${_REMOTE_USER:-vscode}" - local target_home="${_REMOTE_USER_HOME:-/home/${target_user}}" - - # Be defensive: if the resolved home does not exist, fall back to $HOME, - # then to /home/${target_user}. If neither is available, fail clearly. - if [ ! -d "$target_home" ]; then - if [ -n "${HOME:-}" ] && [ -d "$HOME" ]; then - echo "Warning: target_home '$target_home' does not exist, falling back to \$HOME: $HOME" >&2 - target_home="$HOME" - elif [ -d "/home/${target_user}" ]; then - echo "Warning: target_home '$target_home' does not exist, falling back to /home/${target_user}" >&2 - target_home="/home/${target_user}" - else - echo "Error: No suitable home directory found for '${target_user}'. Tried:" >&2 - echo " - _REMOTE_USER_HOME='${_REMOTE_USER_HOME:-}'" >&2 - echo " - \$HOME='${HOME:-}'" >&2 - echo " - /home/${target_user}" >&2 - echo "Please set _REMOTE_USER_HOME to a valid, writable directory." >&2 - exit 1 - fi - fi + # Resolve target user and home (sets TARGET_USER and TARGET_HOME) + resolve_target_home - echo "Target home directory: $target_home" - echo "Target user: $target_user" + echo "Target home directory: $TARGET_HOME" + echo "Target user: $TARGET_USER" - # Create the main .claude directory - mkdir -p "$target_home/.claude" - mkdir -p "$target_home/.claude/agents" - mkdir -p "$target_home/.claude/commands" - mkdir -p "$target_home/.claude/hooks" + # Create the main .claude directory and subdirectories + mkdir -p "$TARGET_HOME/.claude" + mkdir -p "$TARGET_HOME/.claude/agents" + mkdir -p "$TARGET_HOME/.claude/commands" + mkdir -p "$TARGET_HOME/.claude/hooks" # Create empty config files if they don't exist - # This ensures the bind mounts won't fail if files are missing on host - if [ ! -f "$target_home/.claude/.credentials.json" ]; then - echo "{}" > "$target_home/.claude/.credentials.json" - chmod 600 "$target_home/.claude/.credentials.json" + if [ ! -f "$TARGET_HOME/.claude/.credentials.json" ]; then + echo "{}" > "$TARGET_HOME/.claude/.credentials.json" + chmod 600 "$TARGET_HOME/.claude/.credentials.json" fi - if [ ! -f "$target_home/.claude/.claude.json" ]; then - echo "{}" > "$target_home/.claude/.claude.json" - chmod 600 "$target_home/.claude/.claude.json" + if [ ! -f "$TARGET_HOME/.claude/.claude.json" ]; then + echo "{}" > "$TARGET_HOME/.claude/.claude.json" + chmod 600 "$TARGET_HOME/.claude/.claude.json" fi # Set proper ownership - # Note: These will be overridden by bind mounts from the host, - # but this ensures the directories exist with correct permissions - # if the mounts fail or for non-mounted directories if [ "$(id -u)" -eq 0 ]; then - chown -R "$target_user:$target_user" "$target_home/.claude" || true + chown -R "$TARGET_USER:$TARGET_USER" "$TARGET_HOME/.claude" || true fi echo "Claude directories created successfully" } -# Main script starts here +# Main script main() { echo "=========================================" echo "Activating feature 'claude-code' (local)" echo "=========================================" - # Install Claude Code CLI (or verify it's already installed) - if command -v claude >/dev/null; then + # Resolve target user and home (sets TARGET_USER and TARGET_HOME) + resolve_target_home + + local claude_bin="$TARGET_HOME/.pixi/bin/claude" + + # Install Claude Code CLI + if [ -x "$claude_bin" ]; then echo "Claude Code CLI is already installed" - claude --version else install_claude_code || exit 1 fi @@ -123,27 +172,11 @@ main() { echo "Claude Code feature activated successfully!" echo "=========================================" echo "" - echo "Configuration files mounted from host:" - echo " Read-Write (auth & state):" - echo " - ~/.claude/.credentials.json (OAuth tokens)" - echo " - ~/.claude/.claude.json (account, setup tracking)" - echo "" - echo " Read-Only (security-protected):" - echo " - ~/.claude/CLAUDE.md" - echo " - ~/.claude/settings.json" - echo " - ~/.claude/agents/" - echo " - ~/.claude/commands/" - echo " - ~/.claude/hooks/" - echo "" - echo "Authentication:" - echo " - If you're already authenticated on your host, credentials are shared" - echo " - Otherwise, run 'claude' and follow the OAuth flow" - echo " - The OAuth callback may open in your host browser" - echo " - Credentials are stored on your host at ~/.claude/.credentials.json" + echo "Configuration is bind-mounted from the host (~/.claude)" + echo "and persists across container rebuilds." echo "" - echo "To modify config files, edit on your host machine and rebuild the container." + echo "To authenticate, run 'claude' and follow the OAuth flow." echo "" } -# Execute main function main diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 6906159..e6492f7 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,6 +4,7 @@ "dockerfile": "Dockerfile", "context": ".." }, + "initializeCommand": ".devcontainer/claude-code/init-host.sh", "customizations": { "vscode": { "settings": {},