#!/usr/bin/env bash set -euo pipefail APP=opencode MUTED='\033[0;2m' RED='\033[0;31m' ORANGE='\033[38;2;255;140;0m' NC='\033[0m' # No Color requested_version=${VERSION:-} raw_os=$(uname -s) os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]') # Normalize various Unix-like identifiers 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" elif [[ "$arch" == "x86_64" ]]; then arch="x64" fi filename="$APP-$os-$arch.zip" case "$filename" in *"-linux-"*) [[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1 ;; *"-darwin-"*) [[ "$arch" == "x64" || "$arch" == "arm64" ]] || exit 1 ;; *"-windows-"*) [[ "$arch" == "x64" ]] || exit 1 ;; *) echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}" exit 1 ;; esac if ! command -v unzip >/dev/null 2>&1; then echo -e "${RED}Error: 'unzip' is required but not installed.${NC}" exit 1 fi INSTALL_DIR=$HOME/.opencode/bin mkdir -p "$INSTALL_DIR" if [ -z "$requested_version" ]; then url="https://github.com/sst/opencode/releases/latest/download/$filename" specific_version=$(curl -s https://api.github.com/repos/sst/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p') if [[ $? -ne 0 || -z "$specific_version" ]]; then echo -e "${RED}Failed to fetch version information${NC}" exit 1 fi else url="https://github.com/sst/opencode/releases/download/v${requested_version}/$filename" specific_version=$requested_version 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 opencode >/dev/null 2>&1; then opencode_path=$(which opencode) ## TODO: check if version is installed # installed_version=$(opencode version) installed_version="0.0.1" installed_version=$(echo $installed_version | awk '{print $2}') if [[ "$installed_version" != "$specific_version" ]]; then print_message info "${MUTED}Installed version: ${NC}$installed_version." else print_message info "${MUTED}Version ${NC}$specific_version${MUTED} already installed" exit 0 fi fi } unbuffered_sed() { if echo | sed -u -e "" >/dev/null 2>&1; then sed -nu "$@" elif echo | sed -l -e "" >/dev/null 2>&1; then sed -nl "$@" else local pad="$(printf "\n%512s" "")" sed -ne "s/$/\\${pad}/" "$@" 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 tmp_dir=${TMPDIR:-/tmp} local basename="${tmp_dir}/opencode_install_$$" local tracefile="${basename}.trace" rm -f "$tracefile" mkfifo "$tracefile" # Hide cursor printf "\033[?25l" >&4 trap "trap - RETURN; rm -f \"$tracefile\"; printf '\033[?25h' >&4; exec 4>&-" RETURN ( curl --trace-ascii "$tracefile" -s -L -o "$output" "$url" ) & local curl_pid=$! unbuffered_sed \ -e 'y/ACDEGHLNORTV/acdeghlnortv/' \ -e '/^0000: content-length:/p' \ -e '/^<= recv data/p' \ "$tracefile" | \ { local length=0 local bytes=0 while IFS=" " read -r -a line; do [ "${#line[@]}" -lt 2 ] && continue local tag="${line[0]} ${line[1]}" if [ "$tag" = "0000: content-length:" ]; then length="${line[2]}" length=$(echo "$length" | tr -d '\r') bytes=0 elif [ "$tag" = "<= recv" ]; then local size="${line[3]}" bytes=$(( bytes + size )) if [ "$length" -gt 0 ]; then print_progress "$bytes" "$length" fi fi done } wait $curl_pid local ret=$? echo "" >&4 return $ret } download_and_install() { print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version" mkdir -p opencodetmp && cd opencodetmp if [[ "$os" == "windows" ]] || ! download_with_progress "$url" "$filename"; then # Fallback to standard curl on Windows or if custom progress fails curl -# -L -o "$filename" "$url" fi unzip -q "$filename" mv opencode "$INSTALL_DIR" chmod 755 "${INSTALL_DIR}/opencode" cd .. && rm -rf opencodetmp } check_version download_and_install 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" 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="$HOME/.zshrc $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 config_file="" for file in $config_files; do if [[ -f $file ]]; then config_file=$file break fi done if [[ -z $config_file ]]; then print_message error "No config file found for $current_shell. Checked files: ${config_files[@]}" exit 1 fi if [[ ":$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 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}To get started, navigate to a project and run:${NC}" echo -e "opencode ${MUTED}Use free models${NC}" echo -e "opencode auth login ${MUTED}Add paid provider API keys${NC}" echo -e "opencode help ${MUTED}List commands and options${NC}" echo -e "" echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs" echo -e "" echo -e ""