install
Rama dev · réplica de lectura del repositorio original.
#!/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 <<EOF
OpenCode Installer
Usage: install.sh [options]
Options:
-h, --help Display this help message
-v, --version <version> Install a specific version (e.g., 0.0.0-beta-17236)
-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/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
local ret=0
curl -f -# -L -o "$tmp_dir/$filename" "$url" || ret=$?
# curl exits 23 when it cannot write the file, usually because the temporary directory is full.
if [ "$ret" -eq 23 ]; then
print_message error "Could not write the download to $tmp_dir. Make sure it has enough free space, or set TMPDIR to another directory, then try again." >&2
fi
if [ "$ret" -ne 0 ]; then
exit "$ret"
fi
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 <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 ""