#!/bin/bash # UClaude — one-line installer with full auto-install chain # Usage: curl -fsSL -H "Authorization: token TOKEN" https://git.sensey24.ru/aibot777/unlimitedcoding/raw/branch/master/claude/uclaude_install.sh -o /tmp/uclaude.sh && sudo bash /tmp/uclaude.sh set -uo pipefail # Error handler error_handler() { local line_no=$1 echo "ERROR: Command failed at line $line_no" >&2 echo "Last command: $BASH_COMMAND" >&2 echo "Exiting..." >&2 exit 1 } trap 'error_handler $LINENO' ERR # Read-only access token for private repo (scoped: read:repository only) GITEA_TOKEN="${GITEA_TOKEN:-cadffcb0a6a3be728ac1ff619bb40c86588f6837}" REPO_URL="https://x-token:${GITEA_TOKEN}@git.sensey24.ru/aibot777/unlimitedcoding.git" INSTALL_DIR="${UCLAUDE_DIR:-$HOME/unlimitedcoding}" echo "=== UClaude Installer ===" echo " Install dir: $INSTALL_DIR" # ---- Auto-install prerequisites ---- install_pkg() { # Try apt, then yum/dnf, then brew if command -v apt-get >/dev/null 2>&1; then apt-get update -qq && apt-get install -y -qq "$@" elif command -v dnf >/dev/null 2>&1; then dnf install -y -q "$@" elif command -v yum >/dev/null 2>&1; then yum install -y -q "$@" elif command -v brew >/dev/null 2>&1; then brew install "$@" else echo "ERROR: No package manager found. Install $* manually." return 1 fi } need_sudo() { if [ "$(id -u)" -ne 0 ]; then echo " Root privileges required to install packages." echo " Re-run with sudo: curl -fsSL -H 'Authorization: token ${GITEA_TOKEN}' https://git.sensey24.ru/aibot777/unlimitedcoding/raw/branch/master/claude/uclaude_install.sh -o /tmp/uclaude.sh && sudo bash /tmp/uclaude.sh" exit 1 fi } # Git if ! command -v git >/dev/null 2>&1; then echo " git not found, installing..." need_sudo install_pkg git fi # Python 3 if ! command -v python3 >/dev/null 2>&1; then echo " python3 not found, installing..." need_sudo install_pkg python3 fi # curl (needed for nodesource) if ! command -v curl >/dev/null 2>&1; then echo " curl not found, installing..." need_sudo install_pkg curl fi # Node.js — updater handles version check and auto-install if ! command -v node >/dev/null 2>&1; then echo " Node.js not found. Updater will auto-install." fi # ---- Clone / Update repo ---- # Retry helper for git ops — gitea sporadically returns 502 (HTTP) or # closes RPC mid-packfile. Plain `2>/dev/null` masks stderr but the ERR # trap still fires on non-zero exit. Run with retries + clear diagnostic # instead of silent fail. retry_git() { local desc="$1"; shift local attempt for attempt in 1 2 3; do if "$@" 2>&1; then return 0 fi echo " $desc failed (attempt $attempt/3), retrying in 5s..." >&2 sleep 5 done echo " ERROR: $desc failed after 3 attempts" >&2 return 1 } if [ -d "$INSTALL_DIR/.git" ]; then echo " Already cloned, updating..." cd "$INSTALL_DIR" if ! retry_git "git fetch" git fetch --depth 1 origin master; then # Fallback: nuke and re-clone fresh (gitea RPC consistently failing # for incremental fetch happens occasionally; full re-clone is more # robust). echo " Fetch failed permanently — falling back to fresh clone" >&2 cd / rm -rf "$INSTALL_DIR" retry_git "git clone (fresh)" git clone --depth 1 --no-checkout "$REPO_URL" "$INSTALL_DIR" || exit 1 cd "$INSTALL_DIR" git sparse-checkout init --no-cone git sparse-checkout set '/*' 'claude/*' '!claude/releases/v*' 'claude/releases/index.json' 'codex/*' retry_git "git checkout (after fresh clone)" git checkout || exit 1 else retry_git "git reset --hard" git reset --hard origin/master || exit 1 fi else echo " Cloning (shallow, sparse — only latest version)..." # Shallow clone without checkout — retry on transient gitea 502 retry_git "git clone" git clone --depth 1 --no-checkout "$REPO_URL" "$INSTALL_DIR" || exit 1 cd "$INSTALL_DIR" # Enable sparse checkout: root + claude/ + codex/ (so optional codex # install works) + index.json (first pass) git sparse-checkout init --no-cone git sparse-checkout set '/*' 'claude/*' '!claude/releases/v*' 'claude/releases/index.json' 'codex/*' # Allow checkout to fail on transient errors; trap won't catch # because we wrap in `|| true`. git checkout 2>/dev/null || git checkout || exit 1 # Read latest version from index.json and add only that release dir if [ -f claude/releases/index.json ]; then VER=$(python3 -c "import json; print(json.load(open('claude/releases/index.json'))['latest'])") echo " Latest version: v${VER}" git sparse-checkout add "claude/releases/v${VER}" git checkout 2>/dev/null || git checkout || true fi fi echo "" echo " Updating repo to latest version before running updater..." # Update repo to latest version BEFORE running updater (so we get latest # MIN_NODE_VERSION fix). Tolerant of transient gitea 502 — `|| true` # because we already have a working clone, fresh updater can run with # slightly older code if the secondary fetch fails. cd "$INSTALL_DIR" git fetch --depth 1 origin master 2>/dev/null || echo " (secondary fetch failed; continuing with existing clone)" >&2 git reset --hard origin/master 2>/dev/null || git pull --quiet 2>/dev/null || true echo " Running updater..." # Configure npm registry for @anthropic-ai scope before running updater if command -v npm >/dev/null 2>&1; then echo " Configuring npm registry: https://npm.sensey24.ru/" npm config set "@anthropic-ai:registry" "https://npm.sensey24.ru/" 2>/dev/null || true fi # Run updater (needs root for cli.js replacement + node install) if [ "$(id -u)" -eq 0 ]; then python3 claude/uclaude_updater.py --force else echo " Root privileges required to install cli.js." sudo python3 claude/uclaude_updater.py --force fi echo "" echo "=== Installation complete ===" echo "" # Sanity check what landed in settings.json (helps diagnose "only 5 models # in /model picker" case — usually means CLAUDE_CUSTOM_MODELS missing # from settings.json env block). ME_HOME="${HOME:-/root}" SETTINGS="$ME_HOME/.claude/settings.json" if [ -f "$SETTINGS" ]; then echo " Verification: $SETTINGS" python3 - "$SETTINGS" <<'PYEOF' import json, sys try: s = json.load(open(sys.argv[1])) env = s.get("env", {}) ccm = env.get("CLAUDE_CUSTOM_MODELS", "") n = len(ccm.split(",")) if ccm else 0 print(f" base_url: {env.get('ANTHROPIC_BASE_URL','-')}") auth = env.get("ANTHROPIC_AUTH_TOKEN", "") print(f" auth_token: {auth[:8]}{'...' if auth else ' (NOT SET — auth will fail!)'}") print(f" custom_models: {n} models {'⚠ ZERO — /model will show built-ins only' if n==0 else 'OK'}") if n > 0: print(f" sample: {','.join(ccm.split(',')[:3])}...") except Exception as e: print(f" ERROR reading settings.json: {e}") PYEOF else echo " (no $SETTINGS to verify — patcher may not have run)" fi # Verify the installed binary itself carries patcher markers — catches the # case where patcher.config.json + settings.json are correct but the binary # wasn't actually patched (e.g. npm overwrote it after install). echo "" echo " Binary patch verification:" CLAUDE_BIN="$(readlink -f "$(command -v claude 2>/dev/null)" 2>/dev/null || true)" if [ -n "$CLAUDE_BIN" ] && [ -f "$CLAUDE_BIN" ]; then case "$(basename "$CLAUDE_BIN")" in cli.js) if grep -q '__CLAUDE_SETTINGS__\|/\*bypass_permissions_prompt\*/' "$CLAUDE_BIN" 2>/dev/null; then echo " cli.js: OK (patcher markers present)" else echo " cli.js: ⚠ NOT patched ($CLAUDE_BIN — re-run uclaude_updater.py --force)" fi ;; claude.exe|claude) # SEA binary — grep -a treats binary as text sea_ok=true grep -aq '/\*ae1_models_filter_patched' "$CLAUDE_BIN" 2>/dev/null || sea_ok=false grep -aq '/\*bypass_permissions_prompt' "$CLAUDE_BIN" 2>/dev/null || sea_ok=false if $sea_ok; then echo " SEA binary: OK (ae1_models_filter + bypass_permissions markers)" else echo " SEA binary: ⚠ NOT fully patched ($CLAUDE_BIN — re-run uclaude_updater.py --force)" fi ;; *) echo " (unknown artifact: $CLAUDE_BIN)" ;; esac else echo " (no claude binary on PATH — check installation)" fi echo "" echo " Run claude: claude # interactive" echo " Update later: cd $INSTALL_DIR && sudo bash claude/uclaude_update.sh" echo "" echo " To install Codex CLI separately, see README codex section:" echo " https://git.sensey24.ru/aibot777/unlimitedcoding/raw/branch/master/codex/ucodex_install.sh"