Spaces:
Running
Running
| set -euo pipefail | |
| APP=opencode | |
| MUTED='\033[0;2m' | |
| RED='\033[0;31m' | |
| ORANGE='\033[38;5;214m' | |
| NC='\033[0m' # No Color | |
| usage() { | |
| cat <<EOF | |
| OpenCode Installer | |
| Usage: install.sh [options] | |
| Options: | |
| -h, --help Display this help message | |
| -v, --version <version> Install a specific version (e.g., 1.0.180) | |
| -b, --binary <path> 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/install | bash | |
| curl -fsSL https://opencode.ai/install | bash -s -- --version 1.0.180 | |
| ./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 | |
| combo="$os-$arch" | |
| case "$combo" in | |
| linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64) | |
| ;; | |
| *) | |
| echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}" | |
| exit 1 | |
| ;; | |
| esac | |
| archive_ext=".zip" | |
| if [ "$os" = "linux" ]; then | |
| archive_ext=".tar.gz" | |
| fi | |
| 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 | |
| filename="$APP-$target$archive_ext" | |
| if [ "$os" = "linux" ]; then | |
| if ! command -v tar >/dev/null 2>&1; then | |
| echo -e "${RED}Error: 'tar' is required but not installed.${NC}" | |
| exit 1 | |
| fi | |
| else | |
| if ! command -v unzip >/dev/null 2>&1; then | |
| echo -e "${RED}Error: 'unzip' is required but not installed.${NC}" | |
| exit 1 | |
| fi | |
| fi | |
| if [ -z "$requested_version" ]; then | |
| # Fetch latest release version from badman99dev/Opencode | |
| specific_version=$(curl -s https://api.github.com/repos/badman99dev/Opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p') | |
| if [[ $? -ne 0 || -z "$specific_version" ]]; then | |
| echo -e "${RED}Failed to fetch latest version from badman99dev/Opencode${NC}" | |
| exit 1 | |
| fi | |
| url="https://github.com/badman99dev/Opencode/releases/download/v${specific_version}/opencode-linux-x64.tar.gz" | |
| else | |
| # Strip leading 'v' if present | |
| requested_version="${requested_version#v}" | |
| url="https://github.com/badman99dev/Opencode/releases/download/v${requested_version}/opencode-linux-x64.tar.gz" | |
| specific_version=$requested_version | |
| # Verify the release exists before downloading | |
| http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/badman99dev/Opencode/releases/tag/v${requested_version}") | |
| if [ "$http_status" = "404" ]; then | |
| echo -e "${RED}Error: Release v${requested_version} not found${NC}" | |
| echo -e "${MUTED}Available releases: https://github.com/badman99dev/Opencode/releases${NC}" | |
| exit 1 | |
| fi | |
| 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 opencode >/dev/null 2>&1; then | |
| opencode_path=$(which opencode) | |
| ## Check the installed version | |
| installed_version=$(opencode --version 2>/dev/null || echo "") | |
| 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" | |
| 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 -# -L -o "$tmp_dir/$filename" "$url" | |
| fi | |
| if [ "$os" = "linux" ]; then | |
| tar -xzf "$tmp_dir/$filename" -C "$tmp_dir" | |
| # Find and move the opencode binary | |
| OPENCODE_BIN=$(find "$tmp_dir" -name opencode -type f -printf '%h' 2>/dev/null | head -1) | |
| if [ -n "$OPENCODE_BIN" ]; then | |
| mv "$OPENCODE_BIN/opencode" "$INSTALL_DIR" | |
| else | |
| echo -e "${RED}Error: opencode binary not found${NC}" | |
| ls -la "$tmp_dir" | |
| exit 1 | |
| fi | |
| else | |
| unzip -q "$tmp_dir/$filename" -d "$tmp_dir" | |
| fi | |
| chmod 755 "${INSTALL_DIR}/opencode" | |
| rm -rf "$tmp_dir" | |
| } | |
| install_from_binary() { | |
| print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}from: ${NC}$binary_path" | |
| cp "$binary_path" "${INSTALL_DIR}/opencode" | |
| chmod 755 "${INSTALL_DIR}/opencode" | |
| } | |
| if [ -n "$binary_path" ]; then | |
| install_from_binary | |
| else | |
| check_version | |
| download_and_install | |
| fi | |
| 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="${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 | |
| 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 "" | |
| echo -e "cd <project> ${MUTED}# Open directory${NC}" | |
| echo -e "opencode ${MUTED}# Run command${NC}" | |
| echo -e "" | |
| echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs" | |
| echo -e "" | |
| echo -e "" | |