#!/bin/sh set -eu REPO_ARCHIVE_URL="https://github.com/Alishahryar1/free-claude-code/archive/refs/heads/main.zip" PYTHON_VERSION="3.14.0" MIN_UV_VERSION="0.11.0" CLAUDE_INSTALL_URL="https://claude.ai/install.sh" CODEX_INSTALL_URL="https://chatgpt.com/codex/install.sh" PI_INSTALL_URL="https://pi.dev/install.sh" UV_INSTALL_URL="https://astral.sh/uv/install.sh" dry_run=0 voice_nim=0 voice_local=0 voice_all=0 torch_backend="" temporary_script="" show_usage() { cat <<'USAGE' Usage: install.sh [options] Installs Claude Code, Codex, and Pi if missing, ensures a compatible uv, and installs or updates 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 1 ]; then return 0 fi if "$@"; then return 0 else status=$? fi fail "Command failed with exit code $status: $1" } cleanup() { if [ -n "$temporary_script" ] && [ -e "$temporary_script" ]; then rm -f "$temporary_script" fi } trap cleanup EXIT trap 'exit 130' INT trap 'exit 143' HUP TERM add_path_entry() { [ -n "$1" ] || return 0 case ":$PATH:" in *":$1:"*) ;; *) PATH="$1:$PATH" ;; esac } add_known_bin_directories() { 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" add_path_entry "${XDG_DATA_HOME:-$HOME/.local/share}/pi-node/current/bin" fi export PATH hash -r 2>/dev/null || true } add_pi_bin_directories() { [ "$dry_run" -eq 0 ] || return 0 add_known_bin_directories if command -v npm >/dev/null 2>&1; then pi_npm_prefix=$(npm prefix -g 2>/dev/null || npm config get prefix 2>/dev/null || true) if [ -n "$pi_npm_prefix" ]; then add_path_entry "$pi_npm_prefix/bin" export PATH hash -r 2>/dev/null || true fi fi } 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 } download_and_run() { url=$1 interpreter=$2 label=$3 non_interactive=${4:-0} if [ "$dry_run" -eq 1 ]; then print_command curl -fsSL "$url" -o "" if [ "$non_interactive" -eq 1 ]; then printf '+ CODEX_NON_INTERACTIVE=1 ' quote_arg "$interpreter" printf ' \n' else print_command "$interpreter" "" fi return 0 fi temporary_script=$(mktemp "${TMPDIR:-/tmp}/fcc-install.XXXXXX") || fail "Unable to create a temporary file for $label." print_command curl -fsSL "$url" -o "$temporary_script" if curl -fsSL "$url" -o "$temporary_script"; then : else status=$? fail "Could not download the $label installer (curl exit code $status)." fi if [ ! -s "$temporary_script" ]; then fail "The downloaded $label installer was empty." fi if [ "$non_interactive" -eq 1 ]; then printf '+ CODEX_NON_INTERACTIVE=1 ' quote_arg "$interpreter" printf ' ' quote_arg "$temporary_script" printf '\n' if CODEX_NON_INTERACTIVE=1 "$interpreter" "$temporary_script"; then : else status=$? fail "$label installation failed with exit code $status." fi else print_command "$interpreter" "$temporary_script" if "$interpreter" "$temporary_script"; then : else status=$? fail "$label installation failed with exit code $status." fi fi rm -f "$temporary_script" temporary_script="" } verify_command() { command_name=$1 display_name=$2 if [ "$dry_run" -eq 1 ]; then print_command "$command_name" --version return 0 fi command_path=$(command -v "$command_name" 2>/dev/null) || fail "$display_name was installed, but '$command_name' is not available on PATH." run "$command_path" --version } pi_command_is_compatible() { pi_command_path=$(command -v pi 2>/dev/null) || return 1 pi_help=$("$pi_command_path" --help 2>/dev/null) || return 1 case "$pi_help" in *--extension*) ;; *) return 1 ;; esac case "$pi_help" in *--models*) return 0 ;; *) return 1 ;; esac } verify_pi_command() { if [ "$dry_run" -eq 1 ]; then printf '+ pi --help (verify --extension and --models support)\n' print_command pi --version return 0 fi pi_command_path=$(command -v pi 2>/dev/null) || fail "Pi was installed, but 'pi' is not available on PATH." pi_command_is_compatible || fail "The 'pi' command at $pi_command_path is not a compatible Pi Coding Agent." run "$pi_command_path" --version } ensure_claude() { if command -v claude >/dev/null 2>&1; then printf 'Claude Code already found on PATH; verifying it.\n' else download_and_run "$CLAUDE_INSTALL_URL" bash "Claude Code" add_known_bin_directories fi verify_command claude "Claude Code" } ensure_codex() { if command -v codex >/dev/null 2>&1; then printf 'Codex already found on PATH; verifying it.\n' else download_and_run "$CODEX_INSTALL_URL" sh "Codex" 1 add_known_bin_directories fi verify_command codex "Codex" } ensure_pi() { if [ "$dry_run" -eq 1 ] && command -v pi >/dev/null 2>&1; then printf 'Pi already found on PATH; verifying it.\n' elif pi_command_is_compatible; then printf 'Pi already found on PATH; verifying it.\n' else if existing_pi_path=$(command -v pi 2>/dev/null); then printf "The existing 'pi' command at %s is not Pi Coding Agent; installing Pi.\n" "$existing_pi_path" fi download_and_run "$PI_INSTALL_URL" sh "Pi" add_pi_bin_directories fi verify_pi_command } current_uv_version() { if output=$(uv --version); then : else return 1 fi case "$output" in uv\ *) version=${output#uv } ;; *) version=$output ;; esac version=${version%% *} case "$version" in [0-9]*.[0-9]*.[0-9]*) printf '%s\n' "$version" ;; *) return 1 ;; esac } 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 case "$current_major$current_minor$current_patch$minimum_major$minimum_minor$minimum_patch" in *[!0-9]*) return 1 ;; esac [ "$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" ] } verify_uv() { if [ "$dry_run" -eq 1 ]; then print_command uv --version return 0 fi command -v uv >/dev/null 2>&1 || fail "uv was installed, but it is not available on PATH." version=$(current_uv_version) || fail "uv is present, but 'uv --version' did not return a valid version." if ! version_ge "$version" "$MIN_UV_VERSION"; then fail "uv $MIN_UV_VERSION or newer is required; found uv $version after installation." fi printf 'Verified uv %s.\n' "$version" } ensure_uv() { if [ "$dry_run" -eq 1 ]; then if command -v uv >/dev/null 2>&1; then print_command uv --version printf 'A compatible existing uv will be left unchanged; an obsolete one will be replaced by the standalone installer.\n' else printf 'uv is not installed; the current standalone uv would be installed.\n' download_and_run "$UV_INSTALL_URL" sh "uv" verify_uv fi return 0 fi if command -v uv >/dev/null 2>&1; then version=$(current_uv_version) || fail "uv is present, but 'uv --version' did not return a valid version." if version_ge "$version" "$MIN_UV_VERSION"; then printf 'uv %s already satisfies >=%s; leaving it unchanged.\n' "$version" "$MIN_UV_VERSION" return 0 fi printf 'uv %s is below %s; installing the current standalone uv.\n' "$version" "$MIN_UV_VERSION" else printf 'uv is not installed; installing the current standalone uv.\n' fi download_and_run "$UV_INSTALL_URL" sh "uv" add_known_bin_directories verify_uv } 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 [ "$include_nim" -eq 1 ] && [ "$include_local" -eq 1 ]; then printf 'free-claude-code[voice,voice_local] @ %s' "$REPO_ARCHIVE_URL" elif [ "$include_nim" -eq 1 ]; then printf 'free-claude-code[voice] @ %s' "$REPO_ARCHIVE_URL" elif [ "$include_local" -eq 1 ]; then printf 'free-claude-code[voice_local] @ %s' "$REPO_ARCHIVE_URL" else printf 'free-claude-code @ %s' "$REPO_ARCHIVE_URL" fi } install_free_claude_code() { spec=$(package_spec) if [ -n "$torch_backend" ]; then run uv tool install --force --refresh-package free-claude-code --python "$PYTHON_VERSION" --torch-backend "$torch_backend" "$spec" else run uv tool install --force --refresh-package free-claude-code --python "$PYTHON_VERSION" "$spec" fi } configure_and_verify_free_claude_code() { run uv tool update-shell if [ "$dry_run" -eq 1 ]; then print_command uv tool dir --bin printf '+ verify fcc-server, fcc-claude, fcc-codex, and fcc-pi in the uv tool bin directory\n' print_command fcc-server --version return 0 fi print_command uv tool dir --bin if tool_bin=$(uv tool dir --bin); then : else status=$? fail "Could not determine the uv tool bin directory (exit code $status)." fi [ -n "$tool_bin" ] || fail "uv returned an empty tool bin directory." add_path_entry "$tool_bin" export PATH hash -r 2>/dev/null || true for command_name in fcc-server fcc-claude fcc-codex fcc-pi; do [ -x "$tool_bin/$command_name" ] || fail "Free Claude Code installation did not create $tool_bin/$command_name." done run "$tool_bin/fcc-server" --version } parse_args "$@" validate_args add_known_bin_directories step "Checking installation prerequisites" require_command curl require_command bash require_command sh require_command mktemp step "Ensuring Claude Code is installed" ensure_claude step "Ensuring Codex is installed" ensure_codex step "Ensuring Pi is installed" ensure_pi step "Ensuring uv $MIN_UV_VERSION or newer is installed" ensure_uv step "Installing or updating Free Claude Code" install_free_claude_code step "Configuring PATH and verifying Free Claude Code" configure_and_verify_free_claude_code if [ "$dry_run" -eq 1 ]; then printf '\nDry run complete. No changes were made.\n' else printf '\nFree Claude Code is installed and verified. Start the proxy with: fcc-server\n' printf 'Run Claude Code with: fcc-claude\n' printf 'Run Codex with: fcc-codex\n' printf 'Run Pi with: fcc-pi\n' fi