install 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. APP=opencode
  4. MUTED='\033[0;2m'
  5. RED='\033[0;31m'
  6. ORANGE='\033[38;5;214m'
  7. NC='\033[0m' # No Color
  8. usage() {
  9. cat <<EOF
  10. OpenCode Installer
  11. Usage: install.sh [options]
  12. Options:
  13. -h, --help Display this help message
  14. -v, --version <version> Install a specific version (e.g., 1.0.180)
  15. -b, --binary <path> Install from a local binary instead of downloading
  16. --no-modify-path Don't modify shell config files (.zshrc, .bashrc, etc.)
  17. Examples:
  18. curl -fsSL https://opencode.ai/install | bash
  19. curl -fsSL https://opencode.ai/install | bash -s -- --version 1.0.180
  20. ./install --binary /path/to/opencode
  21. EOF
  22. }
  23. requested_version=${VERSION:-}
  24. no_modify_path=false
  25. binary_path=""
  26. while [[ $# -gt 0 ]]; do
  27. case "$1" in
  28. -h|--help)
  29. usage
  30. exit 0
  31. ;;
  32. -v|--version)
  33. if [[ -n "${2:-}" ]]; then
  34. requested_version="$2"
  35. shift 2
  36. else
  37. echo -e "${RED}Error: --version requires a version argument${NC}"
  38. exit 1
  39. fi
  40. ;;
  41. -b|--binary)
  42. if [[ -n "${2:-}" ]]; then
  43. binary_path="$2"
  44. shift 2
  45. else
  46. echo -e "${RED}Error: --binary requires a path argument${NC}"
  47. exit 1
  48. fi
  49. ;;
  50. --no-modify-path)
  51. no_modify_path=true
  52. shift
  53. ;;
  54. *)
  55. echo -e "${ORANGE}Warning: Unknown option '$1'${NC}" >&2
  56. shift
  57. ;;
  58. esac
  59. done
  60. INSTALL_DIR=$HOME/.opencode/bin
  61. mkdir -p "$INSTALL_DIR"
  62. # If --binary is provided, skip all download/detection logic
  63. if [ -n "$binary_path" ]; then
  64. if [ ! -f "$binary_path" ]; then
  65. echo -e "${RED}Error: Binary not found at ${binary_path}${NC}"
  66. exit 1
  67. fi
  68. specific_version="local"
  69. else
  70. raw_os=$(uname -s)
  71. os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]')
  72. case "$raw_os" in
  73. Darwin*) os="darwin" ;;
  74. Linux*) os="linux" ;;
  75. MINGW*|MSYS*|CYGWIN*) os="windows" ;;
  76. esac
  77. arch=$(uname -m)
  78. if [[ "$arch" == "aarch64" ]]; then
  79. arch="arm64"
  80. fi
  81. if [[ "$arch" == "x86_64" ]]; then
  82. arch="x64"
  83. fi
  84. if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then
  85. rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)
  86. if [ "$rosetta_flag" = "1" ]; then
  87. arch="arm64"
  88. fi
  89. fi
  90. combo="$os-$arch"
  91. case "$combo" in
  92. linux-x64|linux-arm64|darwin-x64|darwin-arm64|windows-x64)
  93. ;;
  94. *)
  95. echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}"
  96. exit 1
  97. ;;
  98. esac
  99. archive_ext=".zip"
  100. if [ "$os" = "linux" ]; then
  101. archive_ext=".tar.gz"
  102. fi
  103. is_musl=false
  104. if [ "$os" = "linux" ]; then
  105. if [ -f /etc/alpine-release ]; then
  106. is_musl=true
  107. fi
  108. if command -v ldd >/dev/null 2>&1; then
  109. if ldd --version 2>&1 | grep -qi musl; then
  110. is_musl=true
  111. fi
  112. fi
  113. fi
  114. needs_baseline=false
  115. if [ "$arch" = "x64" ]; then
  116. if [ "$os" = "linux" ]; then
  117. if ! grep -qwi avx2 /proc/cpuinfo 2>/dev/null; then
  118. needs_baseline=true
  119. fi
  120. fi
  121. if [ "$os" = "darwin" ]; then
  122. avx2=$(sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0)
  123. if [ "$avx2" != "1" ]; then
  124. needs_baseline=true
  125. fi
  126. fi
  127. if [ "$os" = "windows" ]; then
  128. ps="(Add-Type -MemberDefinition \"[DllImport(\"\"kernel32.dll\"\")] public static extern bool IsProcessorFeaturePresent(int ProcessorFeature);\" -Name Kernel32 -Namespace Win32 -PassThru)::IsProcessorFeaturePresent(40)"
  129. out=""
  130. if command -v powershell.exe >/dev/null 2>&1; then
  131. out=$(powershell.exe -NoProfile -NonInteractive -Command "$ps" 2>/dev/null || true)
  132. elif command -v pwsh >/dev/null 2>&1; then
  133. out=$(pwsh -NoProfile -NonInteractive -Command "$ps" 2>/dev/null || true)
  134. fi
  135. out=$(echo "$out" | tr -d '\r' | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')
  136. if [ "$out" != "true" ] && [ "$out" != "1" ]; then
  137. needs_baseline=true
  138. fi
  139. fi
  140. fi
  141. target="$os-$arch"
  142. if [ "$needs_baseline" = "true" ]; then
  143. target="$target-baseline"
  144. fi
  145. if [ "$is_musl" = "true" ]; then
  146. target="$target-musl"
  147. fi
  148. filename="$APP-$target$archive_ext"
  149. if [ "$os" = "linux" ]; then
  150. if ! command -v tar >/dev/null 2>&1; then
  151. echo -e "${RED}Error: 'tar' is required but not installed.${NC}"
  152. exit 1
  153. fi
  154. else
  155. if ! command -v unzip >/dev/null 2>&1; then
  156. echo -e "${RED}Error: 'unzip' is required but not installed.${NC}"
  157. exit 1
  158. fi
  159. fi
  160. if [ -z "$requested_version" ]; then
  161. url="https://github.com/anomalyco/opencode/releases/latest/download/$filename"
  162. specific_version=$(curl -s https://api.github.com/repos/anomalyco/opencode/releases/latest | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p')
  163. if [[ $? -ne 0 || -z "$specific_version" ]]; then
  164. echo -e "${RED}Failed to fetch version information${NC}"
  165. exit 1
  166. fi
  167. else
  168. # Strip leading 'v' if present
  169. requested_version="${requested_version#v}"
  170. url="https://github.com/anomalyco/opencode/releases/download/v${requested_version}/$filename"
  171. specific_version=$requested_version
  172. # Verify the release exists before downloading
  173. http_status=$(curl -sI -o /dev/null -w "%{http_code}" "https://github.com/anomalyco/opencode/releases/tag/v${requested_version}")
  174. if [ "$http_status" = "404" ]; then
  175. echo -e "${RED}Error: Release v${requested_version} not found${NC}"
  176. echo -e "${MUTED}Available releases: https://github.com/anomalyco/opencode/releases${NC}"
  177. exit 1
  178. fi
  179. fi
  180. fi
  181. print_message() {
  182. local level=$1
  183. local message=$2
  184. local color=""
  185. case $level in
  186. info) color="${NC}" ;;
  187. warning) color="${NC}" ;;
  188. error) color="${RED}" ;;
  189. esac
  190. echo -e "${color}${message}${NC}"
  191. }
  192. check_version() {
  193. if command -v opencode >/dev/null 2>&1; then
  194. opencode_path=$(which opencode)
  195. ## Check the installed version
  196. installed_version=$(opencode --version 2>/dev/null || echo "")
  197. if [[ "$installed_version" != "$specific_version" ]]; then
  198. print_message info "${MUTED}Installed version: ${NC}$installed_version."
  199. else
  200. print_message info "${MUTED}Version ${NC}$specific_version${MUTED} already installed"
  201. exit 0
  202. fi
  203. fi
  204. }
  205. unbuffered_sed() {
  206. if echo | sed -u -e "" >/dev/null 2>&1; then
  207. sed -nu "$@"
  208. elif echo | sed -l -e "" >/dev/null 2>&1; then
  209. sed -nl "$@"
  210. else
  211. local pad="$(printf "\n%512s" "")"
  212. sed -ne "s/$/\\${pad}/" "$@"
  213. fi
  214. }
  215. print_progress() {
  216. local bytes="$1"
  217. local length="$2"
  218. [ "$length" -gt 0 ] || return 0
  219. local width=50
  220. local percent=$(( bytes * 100 / length ))
  221. [ "$percent" -gt 100 ] && percent=100
  222. local on=$(( percent * width / 100 ))
  223. local off=$(( width - on ))
  224. local filled=$(printf "%*s" "$on" "")
  225. filled=${filled// /■}
  226. local empty=$(printf "%*s" "$off" "")
  227. empty=${empty// /・}
  228. printf "\r${ORANGE}%s%s %3d%%${NC}" "$filled" "$empty" "$percent" >&4
  229. }
  230. download_with_progress() {
  231. local url="$1"
  232. local output="$2"
  233. if [ -t 2 ]; then
  234. exec 4>&2
  235. else
  236. exec 4>/dev/null
  237. fi
  238. local tmp_dir=${TMPDIR:-/tmp}
  239. local basename="${tmp_dir}/opencode_install_$$"
  240. local tracefile="${basename}.trace"
  241. rm -f "$tracefile"
  242. mkfifo "$tracefile"
  243. # Hide cursor
  244. printf "\033[?25l" >&4
  245. trap "trap - RETURN; rm -f \"$tracefile\"; printf '\033[?25h' >&4; exec 4>&-" RETURN
  246. (
  247. curl --trace-ascii "$tracefile" -s -L -o "$output" "$url"
  248. ) &
  249. local curl_pid=$!
  250. unbuffered_sed \
  251. -e 'y/ACDEGHLNORTV/acdeghlnortv/' \
  252. -e '/^0000: content-length:/p' \
  253. -e '/^<= recv data/p' \
  254. "$tracefile" | \
  255. {
  256. local length=0
  257. local bytes=0
  258. while IFS=" " read -r -a line; do
  259. [ "${#line[@]}" -lt 2 ] && continue
  260. local tag="${line[0]} ${line[1]}"
  261. if [ "$tag" = "0000: content-length:" ]; then
  262. length="${line[2]}"
  263. length=$(echo "$length" | tr -d '\r')
  264. bytes=0
  265. elif [ "$tag" = "<= recv" ]; then
  266. local size="${line[3]}"
  267. bytes=$(( bytes + size ))
  268. if [ "$length" -gt 0 ]; then
  269. print_progress "$bytes" "$length"
  270. fi
  271. fi
  272. done
  273. }
  274. wait $curl_pid
  275. local ret=$?
  276. echo "" >&4
  277. return $ret
  278. }
  279. download_and_install() {
  280. print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}version: ${NC}$specific_version"
  281. local tmp_dir="${TMPDIR:-/tmp}/opencode_install_$$"
  282. mkdir -p "$tmp_dir"
  283. if [[ "$os" == "windows" ]] || ! [ -t 2 ] || ! download_with_progress "$url" "$tmp_dir/$filename"; then
  284. # Fallback to standard curl on Windows, non-TTY environments, or if custom progress fails
  285. curl -# -L -o "$tmp_dir/$filename" "$url"
  286. fi
  287. if [ "$os" = "linux" ]; then
  288. tar -xzf "$tmp_dir/$filename" -C "$tmp_dir"
  289. else
  290. unzip -q "$tmp_dir/$filename" -d "$tmp_dir"
  291. fi
  292. mv "$tmp_dir/opencode" "$INSTALL_DIR"
  293. chmod 755 "${INSTALL_DIR}/opencode"
  294. rm -rf "$tmp_dir"
  295. }
  296. install_from_binary() {
  297. print_message info "\n${MUTED}Installing ${NC}opencode ${MUTED}from: ${NC}$binary_path"
  298. cp "$binary_path" "${INSTALL_DIR}/opencode"
  299. chmod 755 "${INSTALL_DIR}/opencode"
  300. }
  301. if [ -n "$binary_path" ]; then
  302. install_from_binary
  303. else
  304. check_version
  305. download_and_install
  306. fi
  307. add_to_path() {
  308. local config_file=$1
  309. local command=$2
  310. if grep -Fxq "$command" "$config_file"; then
  311. print_message info "Command already exists in $config_file, skipping write."
  312. elif [[ -w $config_file ]]; then
  313. echo -e "\n# opencode" >> "$config_file"
  314. echo "$command" >> "$config_file"
  315. print_message info "${MUTED}Successfully added ${NC}opencode ${MUTED}to \$PATH in ${NC}$config_file"
  316. else
  317. print_message warning "Manually add the directory to $config_file (or similar):"
  318. print_message info " $command"
  319. fi
  320. }
  321. XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config}
  322. current_shell=$(basename "$SHELL")
  323. case $current_shell in
  324. fish)
  325. config_files="$HOME/.config/fish/config.fish"
  326. ;;
  327. zsh)
  328. config_files="${ZDOTDIR:-$HOME}/.zshrc ${ZDOTDIR:-$HOME}/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv"
  329. ;;
  330. bash)
  331. config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
  332. ;;
  333. ash)
  334. config_files="$HOME/.ashrc $HOME/.profile /etc/profile"
  335. ;;
  336. sh)
  337. config_files="$HOME/.ashrc $HOME/.profile /etc/profile"
  338. ;;
  339. *)
  340. # Default case if none of the above matches
  341. config_files="$HOME/.bashrc $HOME/.bash_profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile"
  342. ;;
  343. esac
  344. if [[ "$no_modify_path" != "true" ]]; then
  345. config_file=""
  346. for file in $config_files; do
  347. if [[ -f $file ]]; then
  348. config_file=$file
  349. break
  350. fi
  351. done
  352. if [[ -z $config_file ]]; then
  353. print_message warning "No config file found for $current_shell. You may need to manually add to PATH:"
  354. print_message info " export PATH=$INSTALL_DIR:\$PATH"
  355. elif [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
  356. case $current_shell in
  357. fish)
  358. add_to_path "$config_file" "fish_add_path $INSTALL_DIR"
  359. ;;
  360. zsh)
  361. add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
  362. ;;
  363. bash)
  364. add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
  365. ;;
  366. ash)
  367. add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
  368. ;;
  369. sh)
  370. add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH"
  371. ;;
  372. *)
  373. export PATH=$INSTALL_DIR:$PATH
  374. print_message warning "Manually add the directory to $config_file (or similar):"
  375. print_message info " export PATH=$INSTALL_DIR:\$PATH"
  376. ;;
  377. esac
  378. fi
  379. fi
  380. if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then
  381. echo "$INSTALL_DIR" >> $GITHUB_PATH
  382. print_message info "Added $INSTALL_DIR to \$GITHUB_PATH"
  383. fi
  384. echo -e ""
  385. echo -e "${MUTED}  ${NC} ▄ "
  386. echo -e "${MUTED}█▀▀█ █▀▀█ █▀▀█ █▀▀▄ ${NC}█▀▀▀ █▀▀█ █▀▀█ █▀▀█"
  387. echo -e "${MUTED}█░░█ █░░█ █▀▀▀ █░░█ ${NC}█░░░ █░░█ █░░█ █▀▀▀"
  388. echo -e "${MUTED}▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ${NC}▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"
  389. echo -e ""
  390. echo -e ""
  391. echo -e "${MUTED}OpenCode includes free models, to start:${NC}"
  392. echo -e ""
  393. echo -e "cd <project> ${MUTED}# Open directory${NC}"
  394. echo -e "opencode ${MUTED}# Run command${NC}"
  395. echo -e ""
  396. echo -e "${MUTED}For more information visit ${NC}https://opencode.ai/docs"
  397. echo -e ""
  398. echo -e ""