#!/bin/sh set -eu REPO_GIT_URL="git+https://github.com/Alishahryar1/free-claude-code.git" PYTHON_VERSION="3.14.0" MIN_UV_VERSION="0.11.0" UV_INSTALL_URL="https://astral.sh/uv/install.sh" dry_run=0 voice_nim=0 voice_local=0 voice_all=0 torch_backend="" show_usage() { cat <<'USAGE' Usage: install.sh [options] Installs Claude Code and Codex if missing, installs or updates uv, Python 3.14.0, and Free Claude Code. Options: --voice-nim Install NVIDIA NIM voice transcription support. --voice-local Install local Whisper voice transcription support. --voice-all Install all voice transcription backends. --torch-backend VALUE Use a uv PyTorch backend, such as cu130. Requires local voice. --dry-run Print commands without running them. --help Show this help text. USAGE } fail() { printf 'error: %s\n' "$*" >&2 exit 1 } step() { printf '\n==> %s\n' "$1" } quote_arg() { case "$1" in *[!A-Za-z0-9_./:@%+=,-]*|"") escaped=$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g') printf '"%s"' "$escaped" ;; *) printf '%s' "$1" ;; esac } print_command() { printf '+' for arg in "$@"; do printf ' ' quote_arg "$arg" done printf '\n' } run() { print_command "$@" if [ "$dry_run" -eq 0 ]; then "$@" fi } run_uv_installer() { printf '+ curl -LsSf %s | sh\n' "$UV_INSTALL_URL" if [ "$dry_run" -eq 0 ]; then command -v curl >/dev/null 2>&1 || fail "curl is required to install uv." curl -LsSf "$UV_INSTALL_URL" | sh fi } add_path_entry() { [ -n "$1" ] || return 0 case ":$PATH:" in *":$1:"*) ;; *) PATH="$1:$PATH" ;; esac } add_uv_to_path() { if [ -n "${XDG_BIN_HOME:-}" ]; then add_path_entry "$XDG_BIN_HOME" fi if [ -n "${HOME:-}" ]; then add_path_entry "$HOME/.local/bin" add_path_entry "$HOME/.cargo/bin" fi export PATH } require_command() { if [ "$dry_run" -eq 0 ] && ! command -v "$1" >/dev/null 2>&1; then fail "$1 is required. Install it first, then rerun this installer." fi } current_uv_version() { version=$(uv self version --short 2>/dev/null || true) if [ -z "$version" ]; then version=$(uv --version 2>/dev/null | sed 's/^uv //; s/ .*//' || true) fi [ -n "$version" ] || return 1 printf '%s\n' "$version" } version_ge() { current=${1%%[-+]*} minimum=${2%%[-+]*} old_ifs=$IFS IFS=. set -- $current current_major=${1:-0} current_minor=${2:-0} current_patch=${3:-0} set -- $minimum minimum_major=${1:-0} minimum_minor=${2:-0} minimum_patch=${3:-0} IFS=$old_ifs [ "$current_major" -gt "$minimum_major" ] && return 0 [ "$current_major" -lt "$minimum_major" ] && return 1 [ "$current_minor" -gt "$minimum_minor" ] && return 0 [ "$current_minor" -lt "$minimum_minor" ] && return 1 [ "$current_patch" -ge "$minimum_patch" ] } uv_version_satisfies_minimum() { version=$(current_uv_version) || return 1 version_ge "$version" "$MIN_UV_VERSION" } validate_uv_version() { [ "$dry_run" -eq 1 ] && return 0 version=$(current_uv_version) || fail "Unable to determine uv version." if ! version_ge "$version" "$MIN_UV_VERSION"; then fail "uv $MIN_UV_VERSION or newer is required; found uv $version. Upgrade uv with its installer or package manager, then rerun this installer." fi } uv_self_update_supported() { uv self update --dry-run >/dev/null 2>&1 } uv_installed_by_homebrew() { command -v brew >/dev/null 2>&1 && brew list --versions uv >/dev/null 2>&1 } uv_installed_by_pipx() { command -v pipx >/dev/null 2>&1 && pipx list 2>/dev/null | grep -Eq '(^|[[:space:]])package uv([[:space:]]|$)' } uv_installed_in_active_virtualenv() { [ -n "${VIRTUAL_ENV:-}" ] || return 1 uv_path=$(command -v uv) case "$uv_path" in "$VIRTUAL_ENV"/*) return 0 ;; *) return 1 ;; esac } update_existing_uv() { if uv_self_update_supported; then run uv self update return 0 fi if uv_installed_by_homebrew; then run brew upgrade uv return 0 fi if uv_installed_by_pipx; then run pipx upgrade uv return 0 fi if uv_installed_in_active_virtualenv; then run python -m pip install --upgrade uv return 0 fi if uv_version_satisfies_minimum; then printf 'uv is already installed and satisfies >=%s; skipping automatic uv update because the install source was not detected.\n' "$MIN_UV_VERSION" return 0 fi version=$(current_uv_version 2>/dev/null || printf 'unknown') fail "uv $MIN_UV_VERSION or newer is required; found uv $version. The existing uv install source was not detected. Upgrade uv manually with the package manager that installed it, then rerun this installer." } install_claude_if_missing() { if command -v claude >/dev/null 2>&1; then printf 'Claude Code already found on PATH; skipping install.\n' return 0 fi require_command npm run npm install -g @anthropic-ai/claude-code } install_codex_if_missing() { if command -v codex >/dev/null 2>&1; then printf 'Codex already found on PATH; skipping install.\n' return 0 fi require_command npm run npm install -g @openai/codex } install_or_update_uv() { add_uv_to_path if command -v uv >/dev/null 2>&1; then update_existing_uv validate_uv_version return 0 fi run_uv_installer add_uv_to_path if [ "$dry_run" -eq 0 ] && ! command -v uv >/dev/null 2>&1; then fail "uv was installed, but it is not available on PATH. Open a new terminal or add uv's bin directory to PATH." fi validate_uv_version } parse_args() { while [ "$#" -gt 0 ]; do case "$1" in --voice-nim) voice_nim=1 ;; --voice-local) voice_local=1 ;; --voice-all) voice_all=1 ;; --torch-backend) shift [ "$#" -gt 0 ] || fail "--torch-backend requires a value." torch_backend="$1" [ -n "$torch_backend" ] || fail "--torch-backend requires a non-empty value." ;; --torch-backend=*) torch_backend="${1#*=}" [ -n "$torch_backend" ] || fail "--torch-backend requires a non-empty value." ;; --dry-run) dry_run=1 ;; --help|-h) show_usage exit 0 ;; *) show_usage >&2 fail "unknown option: $1" ;; esac shift done } validate_args() { include_local=$voice_local if [ "$voice_all" -eq 1 ]; then include_local=1 fi if [ -n "$torch_backend" ] && [ "$include_local" -ne 1 ]; then fail "--torch-backend requires --voice-local or --voice-all." fi } package_spec() { include_nim=$voice_nim include_local=$voice_local if [ "$voice_all" -eq 1 ]; then include_nim=1 include_local=1 fi if [ -n "$torch_backend" ] && [ "$include_local" -ne 1 ]; then fail "--torch-backend requires --voice-local or --voice-all." fi if [ "$include_nim" -eq 1 ] && [ "$include_local" -eq 1 ]; then printf 'free-claude-code[voice,voice_local] @ %s' "$REPO_GIT_URL" elif [ "$include_nim" -eq 1 ]; then printf 'free-claude-code[voice] @ %s' "$REPO_GIT_URL" elif [ "$include_local" -eq 1 ]; then printf 'free-claude-code[voice_local] @ %s' "$REPO_GIT_URL" else printf '%s' "$REPO_GIT_URL" fi } install_free_claude_code() { spec=$(package_spec) if [ -n "$torch_backend" ]; then run uv tool install --force --torch-backend "$torch_backend" "$spec" else run uv tool install --force "$spec" fi } parse_args "$@" validate_args step "Installing Claude Code if missing" install_claude_if_missing step "Installing Codex if missing" install_codex_if_missing step "Installing uv if missing, updating if present" install_or_update_uv step "Installing Python $PYTHON_VERSION" run uv python install "$PYTHON_VERSION" step "Installing or updating Free Claude Code" install_free_claude_code printf '\nFree Claude Code is installed. Start the proxy with: fcc-server\n' printf 'Run Claude Code with: fcc-claude\n' printf 'Run Codex with: fcc-codex\n'