#!/usr/bin/env bash set -euo pipefail APP=opencode SOURCE_APP=opencode MUTED='\033[0;2m' RED='\033[0;31m' ORANGE='\033[38;5;214m' NC='\033[0m' # No Color usage() { cat < Install a specific version (e.g., 0.0.0-beta-17236) -b, --binary Install from a local binary instead of downloading --no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.) Examples: curl -fsSL https://opencode.ai/v2/install | bash curl -fsSL https://opencode.ai/v2/install | bash -s -- --version 0.0.0-beta-17236 ./install --binary /path/to/opencode EOF } requested_version=${VERSION:-} no_modify_path=false binary_path="" while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage exit 0 ;; -v|--version) if [[ -n "${2:-}" ]]; then requested_version="$2" shift 2 else echo -e "${RED}Error: --version requires a version argument${NC}" exit 1 fi ;; -b|--binary) if [[ -n "${2:-}" ]]; then binary_path="$2" shift 2 else echo -e "${RED}Error: --binary requires a path argument${NC}" exit 1 fi ;; --no-modify-path) no_modify_path=true shift ;; *) echo -e "${ORANGE}Warning: Unknown option '$1'${NC}" >&2 shift ;; esac done INSTALL_DIR=$HOME/.opencode/bin mkdir -p "$INSTALL_DIR" # If --binary is provided, skip all download/detection logic if [ -n "$binary_path" ]; then if [ ! -f "$binary_path" ]; then echo -e "${RED}Error: Binary not found at ${binary_path}${NC}" exit 1 fi specific_version="local" else raw_os=$(uname -s) os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]') case "$raw_os" in Darwin*) os="darwin" ;; Linux*) os="linux" ;; MINGW*|MSYS*|CYGWIN*) os="windows" ;; esac arch=$(uname -m) if [[ "$arch" == "aarch64" ]]; then arch="arm64" fi if [[ "$arch" == "x86_64" ]]; then arch="x64" fi if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0) if [ "$rosetta_flag" = "1" ]; then arch="arm64" fi fi # Git Bash and MSYS2 run as emulated x64 on Windows ARM64, so `uname -m` reports x86_64. # The host CPU still shows in the `uname -s` suffix and in PROCESSOR_IDENTIFIER. if [ "$os" = "windows" ] && [ "$arch" = "x64" ]; then if [[ "$raw_os" == *-ARM64 || "${PROCESSOR_IDENTIFIER:-}" == ARM* ]]; then arch="arm64" fi fi combo="$os-$arch" case "$combo" in linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64|windows-arm64) ;; *) echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}" exit 1 ;; esac is_musl=false if [ "$os" = "linux" ]; then if [ -f /etc/alpine-release ]; then is_musl=true fi if command -v ldd >/dev/null 2>&1; then if ldd --version 2>&1 | grep -qi musl; then is_musl=true fi fi fi needs_baseline=false if [ "$arch" = "x64" ]; then if [ "$os" = "linux" ]; then if ! grep -qwi avx2 /proc/cpuinfo 2>/dev/null; then needs_baseline=true fi fi if [ "$os" = "darwin" ]; then avx2=$(sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0) if [ "$avx2" != "1" ]; then needs_baseline=true fi fi if [ "$os" = "windows" ]; then ps="(Add-Type -MemberDefinition \"[DllImport(\"\"kernel32.dll\"\")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);\" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)" out="" if command -v powershell.exe >/dev/null 2>&1; then out=$(powershell.exe -NoProfile -NonInteractive -Command "$ps" 2>/dev/null || true) elif command -v pwsh >/dev/null 2>&1; then out=$(pwsh -NoProfile -NonInteractive -Command "$ps" 2>/dev/null || true) fi out=$(echo "$out" | tr -d '\r' | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]') if [ "$out" != "true" ] && [ "$out" != "1" ]; then needs_baseline=true fi fi fi target="$os-$arch" if [ "$needs_baseline" = "true" ]; then target="$target-baseline" fi if [ "$is_musl" = "true" ]; then target="$target-musl" fi if ! command -v tar >/dev/null 2>&1; then echo -e "${RED}Error: 'tar' is required but not installed.${NC}" exit 1 fi package_scope="@opencode" if [ -z "$requested_version" ]; then metadata=$(curl -fsSL https://opencode.ai/update/api/latest/cli/npm || true) specific_version=$(echo "$metadata" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p') package=$(echo "$metadata" | sed -n 's/.*"package":"\([^"]*\)".*/\1/p') if [ -z "$specific_version" ] || [ -z "$package" ]; then echo -e "${RED}Failed to fetch version information${NC}" exit 1 fi package_scope="${package%/cli}" else # Strip leading 'v' if present requested_version="${requested_version#v}" specific_version=$requested_version fi package_name="$package_scope/cli-$target" http_status=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/$package_scope%2fcli-$target/$specific_version" || true) # Older clients install the minimum release before they can migrate package names. if [ "$http_status" = "404" ] && [ -n "$requested_version" ]; then package_name="@opencode-ai/cli-$target" http_status=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/@opencode-ai%2fcli-$target/$specific_version" || true) fi if [ "$http_status" = "404" ]; then echo -e "${RED}Error: Version ${specific_version} is not available for $target${NC}" echo -e "${MUTED}Available versions: https://www.npmjs.com/package/$package_name?activeTab=versions${NC}" exit 1 fi if [ "$http_status" != "200" ]; then echo -e "${RED}Failed to fetch package information${NC}" exit 1 fi filename="cli-$target-$specific_version.tgz" url="https://registry.npmjs.org/$package_name/-/$filename" binary_name="$SOURCE_APP" if [ "$os" = "windows" ]; then binary_name="$SOURCE_APP.exe" fi fi print_message() { local level=$1 local message=$2 local color="" case $level in info) color="${NC}" ;; warning) color="${NC}" ;; error) color="${RED}" ;; esac echo -e "${color}${message}${NC}" } check_version() { if command -v "$APP" >/dev/null 2>&1; then opencode_path=$(which "$APP") ## Check the installed version installed_version=$("$APP" --version 2>/dev/null || echo "") installed_version="${installed_version##* }" installed_version="${installed_version#v}" print_message info "${MUTED}Installed version: ${NC}$installed_version." fi } print_progress() { local bytes="$1" local length="$2" [ "$length" -gt 0 ] || return 0 local width=50 local percent=$(( bytes * 100 / length )) [ "$percent" -gt 100 ] && percent=100 local on=$(( percent * width / 100 )) local off=$(( width - on )) local filled=$(printf "%*s" "$on" "") filled=${filled// /■} local empty=$(printf "%*s" "$off" "") empty=${empty// /・} printf "\r${ORANGE}%s%s %3d%%${NC}" "$filled" "$empty" "$percent" >&4 } download_with_progress() { local url="$1" local output="$2" if [ -t 2 ]; then exec 4>&2 else exec 4>/dev/null fi local headers="${output}.headers" # Hide cursor printf "\033[?25l" >&4 trap "trap - RETURN; rm -f \"$headers\"; printf '\033[?25h' >&4; exec 4>&-" RETURN # Poll the file size on a timer so rendering cost stays fixed no matter how many bytes arrive. # Read the size with ls because busybox wc -c reads the whole file. curl -fsL -D "$headers" -o "$output" "$url" & local curl_pid=$! local length bytes while kill -0 "$curl_pid" 2>/dev/null; do if [ -f "$output" ]; then length=$(grep -i '^content-length:' "$headers" 2>/dev/null | tail -n 1 | tr -dc '0-9' || true) bytes=$(ls -ln "$output" | awk '{print $5}') print_progress "$bytes" "${length:-0}" fi sleep 0.1 2>/dev/null || sleep 1 done local ret=0 wait "$curl_pid" || ret=$? if [ "$ret" -eq 0 ]; then bytes=$(ls -ln "$output" | awk '{print $5}') print_progress "$bytes" "$bytes" fi echo "" >&4 return $ret } download_and_install() { print_message info "\n${MUTED}Installing ${NC}$APP ${MUTED}version: ${NC}$specific_version" local tmp_dir="${TMPDIR:-/tmp}/opencode_install_$$" mkdir -p "$tmp_dir" if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then # Fallback to standard curl on Windows, non-TTY environments, or if custom progress fails curl -f -# -L -o "$tmp_dir/$filename" "$url" fi tar -xzf "$tmp_dir/$filename" -C "$tmp_dir" local installed_binary="$APP" if [ "$os" = "windows" ]; then installed_binary="$APP.exe" fi mv "$tmp_dir/package/bin/$binary_name" "$INSTALL_DIR/$installed_binary" chmod 755 "$INSTALL_DIR/$installed_binary" rm -rf "$tmp_dir" } install_from_binary() { print_message info "\n${MUTED}Installing ${NC}$APP ${MUTED}from: ${NC}$binary_path" local installed_binary="$APP" case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) installed_binary="$APP.exe" ;; esac cp "$binary_path" "$INSTALL_DIR/$installed_binary" chmod 755 "$INSTALL_DIR/$installed_binary" } install_legacy_shim() { local shim_os="${os:-}" if [[ -z "$shim_os" ]]; then case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) shim_os="windows" ;; esac fi rm -f "$INSTALL_DIR/opencode2" "$INSTALL_DIR/opencode2.exe" "$INSTALL_DIR/opencode2.cmd" if [[ "$shim_os" == "windows" ]]; then cat > "$INSTALL_DIR/opencode2.cmd" <<'EOF' @echo off "%~dp0opencode.exe" %* exit /b %errorlevel% EOF return fi cat > "$INSTALL_DIR/opencode2" <<'EOF' #!/bin/sh exec "$(dirname "$0")/opencode" "$@" EOF chmod 755 "$INSTALL_DIR/opencode2" } if [ -n "$binary_path" ]; then install_from_binary else check_version download_and_install fi install_legacy_shim path_added=false add_to_path() { local config_file=$1 local command=$2 if grep -Fxq "$command" "$config_file"; then print_message info "Command already exists in $config_file, skipping write." elif [[ -w $config_file ]]; then echo -e "\n# opencode" >> "$config_file" echo "$command" >> "$config_file" path_added=true print_message info "${MUTED}Successfully added ${NC}opencode ${MUTED}to \$PATH in ${NC}$config_file" else print_message warning "Manually add the directory to $config_file (or similar):" print_message info " $command" fi } XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config} current_shell=$(basename "$SHELL") case $current_shell in fish) config_files="$HOME/.config/fish/config.fish" ;; zsh) config_files="${ZDOTDIR:-$HOME}/.zshrc ${ZDOTDIR:-$HOME}/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv" ;; bash) config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile" ;; ash) config_files="$HOME/.ashrc $HOME/.profile /etc/profile" ;; sh) config_files="$HOME/.ashrc $HOME/.profile /etc/profile" ;; *) # Default case if none of the above matches config_files="$HOME/.bashrc $HOME/.bash_profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile" ;; esac if [[ "$no_modify_path" != "true" ]]; then config_file="" for file in $config_files; do if [[ -f $file ]]; then config_file=$file break fi done # A new Mac has no ~/.zshrc, so create the shell's main config file rather than leaving opencode off PATH. if [[ -z $config_file ]]; then case $current_shell in zsh) new_config_file="${ZDOTDIR:-$HOME}/.zshrc" ;; fish) new_config_file="$HOME/.config/fish/config.fish" ;; *) new_config_file="" ;; esac if [[ -n $new_config_file ]] && mkdir -p "$(dirname "$new_config_file")" 2>/dev/null && touch "$new_config_file" 2>/dev/null; then config_file=$new_config_file fi fi if [[ -z $config_file ]]; then print_message warning "No config file found for $current_shell. You may need to manually add to PATH:" print_message info " export PATH=$INSTALL_DIR:\$PATH" elif [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then case $current_shell in fish) add_to_path "$config_file" "fish_add_path $INSTALL_DIR" ;; zsh) add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH" ;; bash) add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH" ;; ash) add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH" ;; sh) add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH" ;; *) export PATH=$INSTALL_DIR:$PATH print_message warning "Manually add the directory to $config_file (or similar):" print_message info " export PATH=$INSTALL_DIR:\$PATH" ;; esac fi fi if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then echo "$INSTALL_DIR" >> $GITHUB_PATH print_message info "Added $INSTALL_DIR to \$GITHUB_PATH" fi echo -e "" echo -e "${MUTED}  ${NC} ▄ " echo -e "${MUTED}█▀▀█ █▀▀█ █▀▀█ █▀▀▄ ${NC}█▀▀▀ █▀▀█ █▀▀█ █▀▀█" echo -e "${MUTED}█░░█ █░░█ █▀▀▀ █░░█ ${NC}█░░░ █░░█ █░░█ █▀▀▀" echo -e "${MUTED}▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ${NC}▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀" echo -e "" echo -e "" echo -e "${MUTED}OpenCode includes free models, to start:${NC}" echo -e "" if [[ $path_added == "true" ]]; then echo -e "exec \$SHELL ${MUTED}# Reload PATH (or open a new terminal)${NC}" fi echo -e "cd ${MUTED}# Open directory${NC}" echo -e "opencode ${MUTED}# Run command${NC}" echo -e "" echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/v2/docs" echo -e "" echo -e ""