| #!/usr/bin/env bash |
| set -euo pipefail |
| APP=opencode |
|
|
| MUTED='\033[0;2m' |
| RED='\033[0;31m' |
| ORANGE='\033[38;5;214m' |
| NC='\033[0m' |
|
|
| 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 [ -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 |
| |
| specific_version=$(curl -s https://api.github.com/repos/badman99dev/opencode-1/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-1${NC}" |
| exit 1 |
| fi |
| |
| url="https://github.com/badman99dev/opencode-1/releases/download/v${specific_version}/opencode-linux-x64.tar.gz" |
| else |
| |
| requested_version="${requested_version#v}" |
| url="https://github.com/badman99dev/opencode-1/releases/download/v${requested_version}/opencode-linux-x64.tar.gz" |
| specific_version=$requested_version |
|
|
| |
| http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/badman99dev/opencode-1/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-1/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) |
|
|
| |
| 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" |
|
|
| |
| 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 |
| |
| curl - |
| fi |
|
|
| if [ "$os" = "linux" ]; then |
| tar -xzf "$tmp_dir/$filename" -C "$tmp_dir" |
| |
| 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" |
| ;; |
| *) |
| |
| 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 "" |
|
|