#!/usr/bin/env bash set -euo pipefail APP="openchinacode" REPO="${OPENCHINACODE_REPO:-krisshen2021/openchinacode}" INSTALL_DIR="${OPENCHINACODE_INSTALL_DIR:-$HOME/.local/bin}" MUTED='\033[0;2m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[38;5;214m' NC='\033[0m' usage() { cat < Install a specific GitHub release version, for example 0.1.0 -b, --binary Install from a local binary instead of downloading --install-dir Install directory, default: ~/.local/bin --no-modify-path Do not modify shell config files Environment: VERSION Same as --version OPENCHINACODE_REPO GitHub repo, default: krisshen2021/openchinacode OPENCHINACODE_INSTALL_DIR Same as --install-dir Examples: curl -fsSL https://raw.githubusercontent.com/krisshen2021/openchinacode/main/install | bash curl -fsSL https://raw.githubusercontent.com/krisshen2021/openchinacode/main/install | bash -s -- --version 0.1.0 ./install --binary packages/opencode/dist/openchinacode-linux-x64/bin/openchinacode EOF } print_message() { local level="$1" local message="$2" local color="$NC" case "$level" in success) color="$GREEN" ;; warning) color="$ORANGE" ;; error) color="$RED" ;; esac echo -e "${color}${message}${NC}" } requested_version="${VERSION:-}" binary_path="" no_modify_path=false while [[ $# -gt 0 ]]; do case "$1" in -h | --help) usage exit 0 ;; -v | --version) if [[ -z "${2:-}" ]]; then print_message error "Error: --version requires a version argument" exit 1 fi requested_version="$2" shift 2 ;; -b | --binary) if [[ -z "${2:-}" ]]; then print_message error "Error: --binary requires a path argument" exit 1 fi binary_path="$2" shift 2 ;; --install-dir) if [[ -z "${2:-}" ]]; then print_message error "Error: --install-dir requires a directory" exit 1 fi INSTALL_DIR="$2" shift 2 ;; --no-modify-path) no_modify_path=true shift ;; *) print_message warning "Warning: unknown option '$1'" shift ;; esac done require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then print_message error "Error: '$1' is required but not installed" exit 1 fi } detect_platform() { local raw_os raw_arch raw_os="$(uname -s)" raw_arch="$(uname -m)" case "$raw_os" in Darwin*) os="darwin" ;; Linux*) os="linux" ;; MINGW* | MSYS* | CYGWIN*) os="windows" ;; *) print_message error "Unsupported OS: $raw_os" exit 1 ;; esac case "$raw_arch" in x86_64 | amd64) arch="x64" ;; arm64 | aarch64) arch="arm64" ;; *) print_message error "Unsupported architecture: $raw_arch" exit 1 ;; esac if [[ "$os" == "darwin" && "$arch" == "x64" ]]; then local rosetta_flag rosetta_flag="$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0)" if [[ "$rosetta_flag" == "1" ]]; then arch="arm64" fi fi } detect_target() { is_musl=false needs_baseline=false if [[ "$os" == "linux" ]]; then if [[ -f /etc/alpine-release ]]; then is_musl=true elif command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then is_musl=true fi fi if [[ "$arch" == "x64" ]]; then if [[ "$os" == "linux" ]]; then if ! grep -qwi avx2 /proc/cpuinfo 2>/dev/null; then needs_baseline=true fi elif [[ "$os" == "darwin" ]]; then local avx2 avx2="$(sysctl -n hw.optional.avx2_0 2>/dev/null || echo 0)" if [[ "$avx2" != "1" ]]; then needs_baseline=true fi elif [[ "$os" == "windows" ]]; then local ps out 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 case "$target" in linux-x64 | linux-x64-baseline | linux-arm64 | linux-x64-musl | linux-x64-baseline-musl | linux-arm64-musl | darwin-x64 | darwin-x64-baseline | darwin-arm64 | windows-x64 | windows-x64-baseline | windows-arm64) ;; *) print_message error "Unsupported target: $target" exit 1 ;; esac } archive_ext() { if [[ "$os" == "linux" ]]; then echo ".tar.gz" else echo ".zip" fi } installed_binary_name() { if [[ "${os:-}" == "windows" ]]; then echo "$APP.exe" else echo "$APP" fi } resolve_release() { require_cmd curl local ext tag ext="$(archive_ext)" filename="$APP-$target$ext" if [[ -z "$requested_version" ]]; then local api api="https://api.github.com/repos/$REPO/releases/latest" tag="$(curl -fsSL "$api" | sed -n 's/.*"tag_name": *"v\{0,1\}\([^"]*\)".*/\1/p' | head -1)" if [[ -z "$tag" ]]; then print_message error "Failed to read latest release from https://github.com/$REPO/releases" exit 1 fi specific_version="$tag" url="https://github.com/$REPO/releases/latest/download/$filename" else specific_version="${requested_version#v}" url="https://github.com/$REPO/releases/download/v${specific_version}/$filename" if ! curl -fsI "https://github.com/$REPO/releases/tag/v${specific_version}" >/dev/null 2>&1; then print_message error "Release v${specific_version} not found" print_message warning "Available releases: https://github.com/$REPO/releases" exit 1 fi fi } check_existing_version() { local existing="" if command -v "$APP" >/dev/null 2>&1; then existing="$(command -v "$APP")" elif [[ -x "$INSTALL_DIR/$APP" ]]; then existing="$INSTALL_DIR/$APP" elif [[ -x "$INSTALL_DIR/$APP.exe" ]]; then existing="$INSTALL_DIR/$APP.exe" fi if [[ -z "$existing" ]]; then return 0 fi local installed_version installed_version="$("$existing" --version 2>/dev/null || true)" if [[ "$installed_version" == "$specific_version" ]]; then print_message success "OpenChinaCode $specific_version is already installed at $existing" exit 0 fi print_message warning "Existing OpenChinaCode: ${installed_version:-unknown} at $existing" } extract_archive() { local archive="$1" local dest="$2" if [[ "$os" == "linux" ]]; then require_cmd tar tar -xzf "$archive" -C "$dest" else require_cmd unzip unzip -q "$archive" -d "$dest" fi } find_extracted_binary() { local dir="$1" local candidate for candidate in \ "$dir/$APP" \ "$dir/$APP.exe" \ "$dir/bin/$APP" \ "$dir/bin/$APP.exe"; do if [[ -f "$candidate" ]]; then echo "$candidate" return 0 fi done find "$dir" -maxdepth 3 -type f \( -name "$APP" -o -name "$APP.exe" \) | head -1 } install_binary_file() { local source="$1" local name name="$(installed_binary_name)" mkdir -p "$INSTALL_DIR" install -m 755 "$source" "$INSTALL_DIR/$name" } download_and_install() { local tmp archive extracted tmp="$(mktemp -d "${TMPDIR:-/tmp}/openchinacode.XXXXXX")" cleanup_download() { rm -rf "$tmp" } trap cleanup_download RETURN archive="$tmp/$filename" print_message success "Installing OpenChinaCode $specific_version" print_message warning "Downloading $filename" curl --fail --location --progress-bar --output "$archive" "$url" extract_archive "$archive" "$tmp" extracted="$(find_extracted_binary "$tmp")" if [[ -z "$extracted" ]]; then print_message error "Downloaded archive did not contain $APP" exit 1 fi install_binary_file "$extracted" trap - RETURN cleanup_download } install_from_local_binary() { if [[ ! -f "$binary_path" ]]; then print_message error "Binary not found: $binary_path" exit 1 fi print_message success "Installing OpenChinaCode from local binary" install_binary_file "$binary_path" } add_to_path() { local config_file="$1" local command="$2" if grep -Fxq "$command" "$config_file"; then print_message success "PATH entry already exists in $config_file" elif [[ -w "$config_file" ]]; then { echo "" echo "# OpenChinaCode" echo "$command" } >>"$config_file" print_message success "Added $INSTALL_DIR to PATH in $config_file" else print_message warning "Manually add this line to $config_file:" echo " $command" fi } configure_path() { if [[ "$no_modify_path" == "true" ]]; then return 0 fi if [[ ":$PATH:" == *":$INSTALL_DIR:"* ]]; then return 0 fi local xdg_config_home current_shell config_files config_file file xdg_config_home="${XDG_CONFIG_HOME:-$HOME/.config}" current_shell="$(basename "${SHELL:-sh}")" 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 | sh) config_files="$HOME/.ashrc $HOME/.profile /etc/profile" ;; *) config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile" ;; esac 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 shell config file found. Add this to PATH manually:" echo " export PATH=$INSTALL_DIR:\$PATH" return 0 fi if [[ "$current_shell" == "fish" ]]; then add_to_path "$config_file" "fish_add_path $INSTALL_DIR" else add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH" fi } detect_platform if [[ -n "$binary_path" ]]; then specific_version="local" install_from_local_binary else detect_target resolve_release check_existing_version download_and_install fi configure_path if [[ "${GITHUB_ACTIONS:-}" == "true" ]]; then echo "$INSTALL_DIR" >>"$GITHUB_PATH" print_message success "Added $INSTALL_DIR to GITHUB_PATH" fi installed_name="$(installed_binary_name)" echo "" echo -e "${GREEN}OpenChinaCode installed successfully.${NC}" echo "" echo " $INSTALL_DIR/$installed_name" echo "" echo "Start in a project:" echo "" echo " cd " echo " $APP" echo ""