|
| 1 | +#!/bin/bash |
| 2 | +set -euo pipefail |
| 3 | + |
| 4 | +VERSION="${VERSION:-latest}" |
| 5 | +USERNAME="${USERNAME:-automatic}" |
| 6 | + |
| 7 | +# Skip installation if version is "none" |
| 8 | +if [ "${VERSION}" = "none" ]; then |
| 9 | + echo "[claude-code-native] Skipping installation (version=none)" |
| 10 | + exit 0 |
| 11 | +fi |
| 12 | + |
| 13 | +echo "[claude-code-native] Starting installation..." |
| 14 | +echo "[claude-code-native] Version: ${VERSION}" |
| 15 | + |
| 16 | +# === VALIDATE DEPENDENCIES === |
| 17 | +# The official installer (claude.ai/install.sh) requires curl internally |
| 18 | +if ! command -v curl >/dev/null 2>&1; then |
| 19 | + echo "[claude-code-native] ERROR: curl is required" |
| 20 | + echo " Ensure common-utils feature is installed first" |
| 21 | + exit 1 |
| 22 | +fi |
| 23 | + |
| 24 | +# === DETECT USER === |
| 25 | +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then |
| 26 | + if [ -n "${_REMOTE_USER:-}" ]; then |
| 27 | + USERNAME="${_REMOTE_USER}" |
| 28 | + elif getent passwd vscode >/dev/null 2>&1; then |
| 29 | + USERNAME="vscode" |
| 30 | + elif getent passwd node >/dev/null 2>&1; then |
| 31 | + USERNAME="node" |
| 32 | + elif getent passwd codespace >/dev/null 2>&1; then |
| 33 | + USERNAME="codespace" |
| 34 | + else |
| 35 | + USERNAME="root" |
| 36 | + fi |
| 37 | +fi |
| 38 | + |
| 39 | +USER_HOME=$(getent passwd "${USERNAME}" | cut -d: -f6) |
| 40 | +if [ -z "${USER_HOME}" ]; then |
| 41 | + echo "[claude-code-native] ERROR: Could not determine home directory for ${USERNAME}" |
| 42 | + exit 1 |
| 43 | +fi |
| 44 | + |
| 45 | +echo "[claude-code-native] Installing for user: ${USERNAME} (home: ${USER_HOME})" |
| 46 | + |
| 47 | +# === PREPARE DIRECTORIES === |
| 48 | +mkdir -p "${USER_HOME}/.local/bin" |
| 49 | +mkdir -p "${USER_HOME}/.local/share/claude" |
| 50 | +mkdir -p "${USER_HOME}/.local/state" |
| 51 | +mkdir -p "${USER_HOME}/.claude" |
| 52 | +chown -R "${USERNAME}:" "${USER_HOME}/.local" "${USER_HOME}/.claude" |
| 53 | + |
| 54 | +# === DETERMINE TARGET === |
| 55 | +# The official installer accepts: stable, latest, or a specific semver |
| 56 | +TARGET="${VERSION}" |
| 57 | +if [ "${TARGET}" != "latest" ] && [ "${TARGET}" != "stable" ]; then |
| 58 | + if ! echo "${TARGET}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then |
| 59 | + echo "[claude-code-native] ERROR: Invalid version '${TARGET}'" |
| 60 | + echo " Use 'latest', 'stable', or a semver (e.g., 2.1.52)" |
| 61 | + exit 1 |
| 62 | + fi |
| 63 | +fi |
| 64 | + |
| 65 | +# === INSTALL === |
| 66 | +# The official Anthropic installer handles: |
| 67 | +# - Platform detection (linux/darwin, x64/arm64, glibc/musl) |
| 68 | +# - Manifest download and checksum verification |
| 69 | +# - Binary download to ~/.local/bin/claude (symlink to ~/.local/share/claude/versions/*) |
| 70 | +echo "[claude-code-native] Downloading official installer..." |
| 71 | + |
| 72 | +if [ "${USERNAME}" = "root" ]; then |
| 73 | + curl -fsSL https://claude.ai/install.sh | bash -s -- "${TARGET}" |
| 74 | +else |
| 75 | + su - "${USERNAME}" -c "curl -fsSL https://claude.ai/install.sh | bash -s -- \"${TARGET}\"" |
| 76 | +fi |
| 77 | + |
| 78 | +# === VERIFICATION === |
| 79 | +CLAUDE_BIN="${USER_HOME}/.local/bin/claude" |
| 80 | + |
| 81 | +if [ -x "${CLAUDE_BIN}" ]; then |
| 82 | + INSTALLED_VERSION=$(su - "${USERNAME}" -c "${CLAUDE_BIN} --version 2>/dev/null" || echo "unknown") |
| 83 | + echo "[claude-code-native] ✓ Claude Code installed: ${INSTALLED_VERSION}" |
| 84 | + echo "[claude-code-native] Binary: ${CLAUDE_BIN}" |
| 85 | +else |
| 86 | + echo "[claude-code-native] ERROR: Installation failed — ${CLAUDE_BIN} not found or not executable" |
| 87 | + echo "[claude-code-native] Expected binary at: ${CLAUDE_BIN}" |
| 88 | + ls -la "${USER_HOME}/.local/bin/" 2>/dev/null || true |
| 89 | + exit 1 |
| 90 | +fi |
| 91 | + |
| 92 | +# === POST-START HOOK === |
| 93 | +# Ensures hasCompletedOnboarding is set when token auth is configured. |
| 94 | +# Runs as the LAST post-start hook (99- prefix) to catch overwrites from |
| 95 | +# Claude Code CLI/extension that may race with postStartCommand. |
| 96 | +HOOK_DIR="/usr/local/devcontainer-poststart.d" |
| 97 | +mkdir -p "$HOOK_DIR" |
| 98 | +cat > "$HOOK_DIR/99-claude-onboarding.sh" << 'HOOK_EOF' |
| 99 | +#!/bin/bash |
| 100 | +# Ensure hasCompletedOnboarding: true in .claude.json when token auth exists. |
| 101 | +# Runs after all setup scripts to catch any overwrites by Claude Code CLI/extension. |
| 102 | +_USERNAME="${SUDO_USER:-${USER:-vscode}}" |
| 103 | +_USER_HOME=$(getent passwd "$_USERNAME" 2>/dev/null | cut -d: -f6) |
| 104 | +_USER_HOME="${_USER_HOME:-/home/$_USERNAME}" |
| 105 | +CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-${_USER_HOME}/.claude}" |
| 106 | +CLAUDE_JSON="$CLAUDE_DIR/.claude.json" |
| 107 | +CRED_FILE="$CLAUDE_DIR/.credentials.json" |
| 108 | +
|
| 109 | +# Only act when token auth is configured |
| 110 | +[ -f "$CRED_FILE" ] || exit 0 |
| 111 | +
|
| 112 | +if [ -f "$CLAUDE_JSON" ]; then |
| 113 | + if ! grep -q '"hasCompletedOnboarding"' "$CLAUDE_JSON" 2>/dev/null; then |
| 114 | + if command -v jq >/dev/null 2>&1; then |
| 115 | + jq '. + {"hasCompletedOnboarding": true}' "$CLAUDE_JSON" > "${CLAUDE_JSON}.tmp" && \ |
| 116 | + mv "${CLAUDE_JSON}.tmp" "$CLAUDE_JSON" |
| 117 | + else |
| 118 | + sed -i '$ s/}$/,\n "hasCompletedOnboarding": true\n}/' "$CLAUDE_JSON" |
| 119 | + fi |
| 120 | + echo "[claude-onboarding] Injected hasCompletedOnboarding into .claude.json" |
| 121 | + fi |
| 122 | +else |
| 123 | + printf '{\n "hasCompletedOnboarding": true\n}\n' > "$CLAUDE_JSON" |
| 124 | + echo "[claude-onboarding] Created .claude.json with hasCompletedOnboarding" |
| 125 | +fi |
| 126 | +HOOK_EOF |
| 127 | +chmod +x "$HOOK_DIR/99-claude-onboarding.sh" |
| 128 | + |
| 129 | +echo "[claude-code-native] Installation complete" |
0 commit comments