#!/bin/sh # Hugging Face CLI Installer for Linux/MacOS # Usage: curl -LsSf https://hf.co/cli/install.sh | bash -s # Usage: curl -LsSf https://hf.co/cli/install.sh | bash -s --with-transformers # Usage: curl -LsSf https://hf.co/cli/install.sh | bash -s -- [OPTIONS] if [ -z "$BASH_VERSION" ]; then if command -v bash >/dev/null 2>&1; then if [ -f "$0" ] && [ "$0" != "sh" ] && [ "$0" != "bash" ]; then exec bash "$0" "$@" else tmp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t hf-cli-install) tmp_script="$tmp_dir/install.sh" cat >"$tmp_script" chmod +x "$tmp_script" bash "$tmp_script" "$@" exit_code=$? rm -rf "$tmp_dir" exit $exit_code fi else echo "[ERROR] bash is required to run this installer." >&2 echo "[ERROR] Please run: curl -LsSf https://hf.co/cli/install.sh | bash" >&2 exit 1 fi fi set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Logging level: 0=quiet, 1=info (default), 2=verbose LOG_LEVEL=1 # Configuration FORCE_REINSTALL="false" BIN_DIR="${HF_CLI_BIN_DIR:-$HOME/.local/bin}" UPDATED_RC_FILE="" SKIP_PATH_UPDATE="false" UPDATED_FISH_PATH="false" WITH_TRANSFORMERS="false" EXCLUDE_SKILL="false" # Logging functions log_debug() { if [ "$LOG_LEVEL" -lt 2 ]; then return 0 fi printf '%b\n' "${BLUE}[DEBUG]${NC} $1" } log_info() { if [ "$LOG_LEVEL" -lt 1 ]; then return 0 fi printf '%b\n' "${BLUE}[INFO]${NC} $1" } log_success() { printf '%b\n' "${GREEN}[SUCCESS]${NC} $1" } log_warning() { printf '%b\n' "${YELLOW}[WARNING]${NC} $1" } log_error() { printf '%b\n' "${RED}[ERROR]${NC} $1" >&2 } run_command() { local description="$1" shift set +e "$@" local status=$? set -e if [ $status -ne 0 ]; then log_error "$description" log_error "Command: $*" log_error "Re-run with --verbose for detailed output." exit $status fi } usage() { cat <<'EOF' Usage: curl -LsSf https://hf.co/cli/install.sh | bash -s -- [OPTIONS] Options: --force Recreate the Hugging Face CLI virtual environment if it exists --no-modify-path Skip adding the hf wrapper directory to PATH --with-transformers Also install the transformers CLI --exclude-skill Skip installing the `hf-cli` skill for AI agents -v, --verbose Enable verbose output (includes full pip logs) --help, -h Show this message and exit Environment variables: HF_HOME Installation base directory; installer uses $HF_HOME/cli when set HF_CLI_BIN_DIR Directory for the hf wrapper (default: ~/.local/bin) EOF } # Normalize user paths to absolute paths expand_path() { local input="$1" if [ -z "$input" ]; then return 0 fi case "$input" in ~) printf '%s\n' "$HOME" ;; ~/*) printf '%s/%s\n' "$HOME" "${input#~/}" ;; /*) printf '%s\n' "$input" ;; *) printf '%s/%s\n' "$PWD" "$input" ;; esac } while [ $# -gt 0 ]; do case "$1" in --force) FORCE_REINSTALL="true" ;; --no-modify-path) SKIP_PATH_UPDATE="true" ;; --with-transformers) WITH_TRANSFORMERS="true" ;; --exclude-skill) EXCLUDE_SKILL="true" ;; -v|--verbose) LOG_LEVEL=2 HF_CLI_VERBOSE_PIP=1 ;; --help|-h) usage exit 0 ;; *) log_error "Unknown option: $1" usage exit 1 ;; esac shift done # Persist fully resolved paths for downstream use and wrapper creation BIN_DIR=$(expand_path "$BIN_DIR") if [ -n "$HF_HOME" ]; then HF_CLI_DIR="$HF_HOME/cli" else HF_CLI_DIR="$HOME/.hf-cli" fi HF_CLI_DIR=$(expand_path "$HF_CLI_DIR") VENV_DIR="$HF_CLI_DIR/venv" # Check if command exists command_exists() { command -v "$1" >/dev/null 2>&1 } # Detect OS detect_os() { if [[ "$OSTYPE" == "linux-gnu"* ]]; then echo "linux" elif [[ "$OSTYPE" == "darwin"* ]]; then echo "macos" else echo "unknown" fi } # Install Python if not available python_version_supported() { "$1" <<'PY' >/dev/null 2>&1 import sys sys.exit(0 if sys.version_info >= (3, 10) else 1) PY } ensure_python() { local candidates=(python3 python) local chosen="" local version_output="" for candidate in "${candidates[@]}"; do if command_exists "$candidate"; then version_output="$($candidate --version 2>&1)" if python_version_supported "$candidate"; then PYTHON_CMD="$candidate" chosen="$candidate" break else log_warning "$candidate detected ($version_output) but Python 3.10+ is required." fi fi done if [ -z "$chosen" ]; then log_error "Python 3.10+ is required but was not found." case "$(detect_os)" in macos) log_info "On macOS: brew install python (or download Python 3.10+ from python.org)" ;; linux) if command_exists apt-get || command_exists apt; then log_info "On Debian/Ubuntu: sudo apt update && sudo apt install python3 python3-pip" elif command_exists dnf; then log_info "On Fedora/RHEL: sudo dnf install python3 python3-pip" elif command_exists yum; then log_info "On CentOS/RHEL: sudo yum install python3 python3-pip" else log_info "Install Python 3.10+ with your distro's package manager." fi ;; *) log_info "Install Python 3.10+ from https://www.python.org/downloads/" ;; esac exit 1 fi log_info "Using Python: $($PYTHON_CMD --version)" } # Create directories create_directories() { log_info "Creating directories..." run_command "Failed to create install directory $HF_CLI_DIR" mkdir -p "$HF_CLI_DIR" run_command "Failed to create bin directory $BIN_DIR" mkdir -p "$BIN_DIR" } # Create virtual environment create_venv() { log_info "Creating virtual environment..." if [ -d "$VENV_DIR" ]; then if [ "$FORCE_REINSTALL" = "true" ]; then log_warning "Virtual environment already exists; removing it since --force was passed" rm -rf "$VENV_DIR" else log_info "Virtual environment already exists; reusing (pass --force to recreate)" return fi fi # Fail early with guidance when python lacks the venv module if ! "$PYTHON_CMD" -m venv --help >/dev/null 2>&1; then log_error "Python's venv module is unavailable. Install python3-venv / ensurepip and retry." case "$(detect_os)" in linux) log_info "Try: sudo apt install python3-venv # Debian/Ubuntu" log_info " sudo dnf install python3-venv # Fedora" ;; macos) log_info "Try reinstalling Python via Homebrew: brew install python" ;; esac exit 1 fi run_command "Failed to create virtual environment at $VENV_DIR" "$PYTHON_CMD" -m venv "$VENV_DIR" # Mark this installation as installer-managed touch "$VENV_DIR/.hf_installer_marker" # Use the venv python for pip management log_info "Upgrading pip..." run_command "Failed to upgrade pip" "$VENV_DIR/bin/python" -m pip install --upgrade pip } # Install a Python package in the venv install_package() { local package_spec="$1" local extra_pip_args="${HF_CLI_PIP_ARGS:-${HF_PIP_ARGS:-}}" local verbose="${HF_CLI_VERBOSE_PIP:-}" if [ -n "$extra_pip_args" ]; then log_info "Passing extra arguments: $extra_pip_args" fi if [ "$verbose" != "1" ]; then log_info "Installation output suppressed; set HF_CLI_VERBOSE_PIP=1 for full logs" fi # Check if uv is available and use it for faster installation if command_exists uv; then log_info "Using uv for faster installation" local -a uv_flags if [ "$verbose" != "1" ]; then uv_flags=(--quiet) else uv_flags=() fi # shellcheck disable=SC2086 run_command "Failed to install $package_spec" uv pip install --python "$VENV_DIR/bin/python" --upgrade ${uv_flags[*]} "$package_spec" $extra_pip_args else local -a pip_flags if [ "$verbose" != "1" ]; then pip_flags=(--quiet --progress-bar off --disable-pip-version-check) else pip_flags=() fi # shellcheck disable=SC2086 run_command "Failed to install $package_spec" "$VENV_DIR/bin/python" -m pip install --upgrade "$package_spec" ${pip_flags[*]} $extra_pip_args fi } # Install huggingface_hub with CLI extras install_hf_hub() { log_info "Installing/upgrading Hugging Face CLI (latest)..." install_package "huggingface_hub" } # Check if transformers is installed in the venv (using importlib.metadata for speed) transformers_installed() { "$VENV_DIR/bin/python" -c "import importlib.metadata; importlib.metadata.version('transformers')" >/dev/null 2>&1 } # Install transformers CLI install_transformers() { # Install if --with-transformers was passed OR if transformers is already in the venv if [ "$WITH_TRANSFORMERS" != "true" ] && ! transformers_installed; then return fi log_info "Installing/upgrading transformers CLI (latest)..." install_package "transformers" } # Expose the hf CLI by linking or copying the console script from the virtualenv expose_cli_command() { log_info "Linking hf CLI into $BIN_DIR..." local source_cli="$VENV_DIR/bin/hf" if [ ! -x "$source_cli" ]; then log_error "hf command not found in the virtual environment at $source_cli" log_error "Verify that The Hugging Face CLI is installed correctly." exit 1 fi local link_method="" if ln -sf "$source_cli" "$BIN_DIR/hf" 2>/dev/null; then link_method="symlink" else if cp "$source_cli" "$BIN_DIR/hf" 2>/dev/null; then link_method="copy" else log_error "Failed to place hf command in $BIN_DIR (tried symlink and copy)." exit 1 fi fi chmod +x "$BIN_DIR/hf" if [ "$link_method" = "symlink" ]; then log_info "hf available at $BIN_DIR/hf (symlink to venv)" else log_info "hf available at $BIN_DIR/hf" fi log_info "Run without touching PATH: env PATH=\"$BIN_DIR:\$PATH\" hf --help" } # Install the `hf-cli` skill globally (opt-out with --exclude-skill) install_skill() { if [ "$EXCLUDE_SKILL" = "true" ]; then log_info "Skipping the hf-cli skill (--exclude-skill)" return fi log_info "Installing the hf-cli skill for AI agents..." if ! "$BIN_DIR/hf" skills add hf-cli --global --claude --force; then log_warning "Failed to install the hf-cli skill. Install it later with: hf skills add -g --claude" return fi log_info "The hf-cli skill was installed automatically so AI agents know how to use the hf CLI." log_info "Pass --exclude-skill to skip it." } # Update PATH if needed update_path() { local shell_rc="" local -a shell_rc_candidates=() # Broaden shell detection and guidance for PATH propagation case "$SHELL" in */bash) shell_rc_candidates=() shell_rc_candidates+=("$HOME/.bashrc") shell_rc_candidates+=("$HOME/.bash_profile") shell_rc_candidates+=("$HOME/.profile") ;; */zsh) shell_rc_candidates=() shell_rc_candidates+=("$HOME/.zshrc") shell_rc_candidates+=("$HOME/.zprofile") ;; */fish) shell_rc_candidates=() if command -v fish >/dev/null 2>&1; then if fish -c "contains \"$BIN_DIR\" \$fish_user_paths" >/dev/null 2>&1; then log_info "$BIN_DIR already present in fish_user_paths" UPDATED_FISH_PATH="true" return elif fish -c "set -Ux fish_user_paths \"$BIN_DIR\" \$fish_user_paths" >/dev/null 2>&1; then UPDATED_FISH_PATH="true" log_success "Added $BIN_DIR to fish_user_paths" log_info "Apply it now with: set -Ux fish_user_paths $BIN_DIR \$fish_user_paths" return else log_warning "Could not update fish_user_paths automatically." fi fi ;; *) shell_rc_candidates=() shell_rc_candidates+=("$HOME/.profile") ;; esac if [[ ":$PATH:" != *":$BIN_DIR:"* ]]; then if [ "$SKIP_PATH_UPDATE" = "true" ]; then log_info "Skipping PATH update (--no-modify-path)." return fi log_info "$BIN_DIR is not in your PATH" if [ "${#shell_rc_candidates[@]}" -gt 0 ]; then for candidate in "${shell_rc_candidates[@]}"; do if [ -f "$candidate" ]; then shell_rc="$candidate" break fi done if [ -z "$shell_rc" ]; then shell_rc="${shell_rc_candidates[0]}" log_info "Creating shell config file at $shell_rc to update PATH" touch "$shell_rc" fi if ! grep -Fq "$BIN_DIR" "$shell_rc"; then { echo "" echo "# Added by Hugging Face CLI installer" echo "export PATH=\"$BIN_DIR:\$PATH\"" } >> "$shell_rc" UPDATED_RC_FILE="$shell_rc" log_success "Added $BIN_DIR to PATH via $shell_rc" if [ "$LOG_LEVEL" -ge 1 ]; then log_info "Apply it now with: source $shell_rc" fi fi else log_warning "Could not automatically update PATH for your shell." if [[ "$SHELL" == *"/fish" ]]; then if [ "$UPDATED_FISH_PATH" != "true" ]; then log_warning "Run: set -Ux fish_user_paths $BIN_DIR \$fish_user_paths" fi else log_warning "Add this line to your shell config: export PATH=\"$BIN_DIR:\$PATH\"" fi fi fi } # Verify installation verify_installation() { log_info "Verifying installation..." if [ -x "$BIN_DIR/hf" ]; then # Test the CLI if "$BIN_DIR/hf" version >/dev/null 2>&1; then log_success "Hugging Face CLI installed successfully!" log_info "CLI location: $BIN_DIR/hf" log_info "Installation directory: $HF_CLI_DIR" else log_error "Installation verification failed. The hf command is not working properly." exit 1 fi else log_error "Installation failed. Wrapper script not found." exit 1 fi } # Uninstall function show_uninstall_info() { log_info "" log_info "To uninstall the Hugging Face CLI, run:" log_info " rm -rf $HF_CLI_DIR" log_info " rm -f $BIN_DIR/hf" log_info "" if [ -n "$UPDATED_RC_FILE" ]; then log_info " (shell) Undo PATH entry: sed -i.bak '/Added by Hugging Face CLI installer/d' $UPDATED_RC_FILE && rm -f ${UPDATED_RC_FILE}.bak" elif [ "$UPDATED_FISH_PATH" = "true" ]; then log_info " (fish) Undo PATH entry: fish -c 'set -Ux fish_user_paths (string match -v \"$BIN_DIR\" \$fish_user_paths)'" elif [ "$SKIP_PATH_UPDATE" = "true" ]; then log_info " (PATH unchanged because --no-modify-path was used)" else log_info " Remove any PATH edits you made manually." fi } # Main installation process main() { log_info "Installing Hugging Face CLI..." log_info "OS: $(detect_os)" log_info "Force reinstall: $FORCE_REINSTALL" log_info "Install dir: $HF_CLI_DIR" log_info "Bin dir: $BIN_DIR" log_info "Skip PATH update: $SKIP_PATH_UPDATE" log_info "Exclude skill: $EXCLUDE_SKILL" ensure_python create_directories create_venv install_hf_hub install_transformers expose_cli_command install_skill update_path verify_installation if [[ ":$PATH:" == *":$BIN_DIR:"* ]]; then log_info "Current version: $(hf version 2>/dev/null || echo 'Run source ~/.bashrc or restart your shell first')" else log_info "Current version: $($BIN_DIR/hf version)" fi show_uninstall_info log_success "hf CLI ready!" log_info "Binary: $BIN_DIR/hf" log_info "Virtualenv: $HF_CLI_DIR" log_info "Try it now: env PATH=\"$BIN_DIR:\$PATH\" hf --help" log_info "Examples:" log_info " hf auth login" log_info " hf download deepseek-ai/DeepSeek-R1" log_info " hf jobs run python:3.12 python -c 'print(\"Hello from HF CLI!\")'" log_info "" } # Handle Ctrl+C trap 'log_error "Installation interrupted"; exit 130' INT main "$@"