Files
wehub-resource-sync 5296d0e97c
CI / Ban suppressions and legacy annotations (push) Has been cancelled
CI / pytest (push) Has been cancelled
CI / ruff-check (push) Has been cancelled
CI / ruff-format (push) Has been cancelled
CI / ty (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:44 +08:00

500 lines
13 KiB
Bash

#!/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 "<temporary-script>"
if [ "$non_interactive" -eq 1 ]; then
printf '+ CODEX_NON_INTERACTIVE=1 '
quote_arg "$interpreter"
printf ' <temporary-script>\n'
else
print_command "$interpreter" "<temporary-script>"
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