install 11 KB

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