#!/usr/bin/env bash # Qwen Code Installation Script # Installs Qwen Code from a standalone archive when available, with npm fallback. # This script intentionally does not install Node.js or change npm config. # # Usage: # install-qwen-standalone.sh --source [github|npm|internal|local-build] # install-qwen-standalone.sh --method [detect|standalone|npm] if [ -z "${BASH_VERSION}" ] && [ -z "${__QWEN_INSTALL_REEXEC:-}" ]; then if command -v bash >/dev/null 2>&1; then if [ -f "${0}" ]; then export __QWEN_INSTALL_REEXEC=1 exec bash -- "${0}" "$@" fi echo "Error: This script requires bash. Run the installer with: curl ... | bash" exit 1 fi echo "Error: This script requires bash. Please install bash first." exit 1 fi set -eo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' log_info() { printf '%bINFO:%b %s\n' "${BLUE}" "${NC}" "$1" } log_success() { printf '%bSUCCESS:%b %s\n' "${GREEN}" "${NC}" "$1" } log_warning() { printf '%bWARNING:%b %s\n' "${YELLOW}" "${NC}" "$1" } log_error() { printf '%bERROR:%b %s\n' "${RED}" "${NC}" "$1" >&2 } command_exists() { command -v "$1" >/dev/null 2>&1 } TEMP_DIRS=() cleanup_temp_dirs() { local temp_dir for temp_dir in "${TEMP_DIRS[@]}"; do if [[ -n "${temp_dir}" ]]; then rm -rf "${temp_dir}" fi done } register_temp_dir() { local temp_dir="$1" TEMP_DIRS+=("${temp_dir}") } shell_quote() { printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")" } display_install_version() { if [[ "${VERSION}" == "latest" ]]; then echo "latest" return 0 fi echo "${VERSION#v}" } trap cleanup_temp_dirs EXIT trap 'cleanup_temp_dirs; exit 130' INT trap 'cleanup_temp_dirs; exit 143' TERM print_usage() { cat </dev/null || echo unknown)" in Darwin) echo " brew install node" echo " # or download from https://nodejs.org/" ;; Linux) echo " # Use your distribution package manager or:" echo " https://nodejs.org/en/download/package-manager" ;; *) echo " https://nodejs.org/" ;; esac echo "" echo "If you already use a Node version manager, activate Node.js 22+" echo "in this shell before rerunning the installer." } require_node() { if ! command_exists node; then log_error "Node.js was not found." print_node_help return 1 fi local node_version node_version=$(node -p "process.versions.node" 2>/dev/null || true) local node_major node_major=$(node -p "Number(process.versions.node.split('.')[0])" 2>/dev/null || true) if [[ -z "${node_major}" ]] || ! [[ "${node_major}" =~ ^[0-9]+$ ]]; then log_error "Unable to determine Node.js version." print_node_help return 1 fi if [[ "${node_major}" -lt 22 ]]; then log_error "Node.js ${node_version:-unknown} is installed, but Node.js 22 or newer is required." print_node_help return 1 fi log_success "Node.js ${node_version} detected." } require_npm() { if command_exists npm; then log_success "npm $(npm -v 2>/dev/null || echo unknown) detected." return 0 fi log_error "npm was not found." echo "" echo "Please install Node.js with npm included, then rerun this installer." echo "Download Node.js from https://nodejs.org/ if your package manager" echo "installed Node without npm." return 1 } get_npm_global_bin() { local prefix prefix=$(npm prefix -g 2>/dev/null || true) if [[ -z "${prefix}" ]]; then return 0 fi case "$(uname -s 2>/dev/null || echo unknown)" in MINGW*|MSYS*|CYGWIN*) echo "${prefix}" ;; *) echo "${prefix}/bin" ;; esac } get_npm_global_root() { npm root -g 2>/dev/null || true } create_source_json() { if [[ "${SOURCE}" == "unknown" ]]; then return 0 fi local qwen_dir="${HOME}/.qwen" mkdir -p "${qwen_dir}" local escaped_source escaped_source=$(printf '%s' "${SOURCE}" | sed 's/\\/\\\\/g; s/"/\\"/g') cat > "${qwen_dir}/source.json" </dev/null || echo unknown) local arch arch=$(uname -m 2>/dev/null || echo unknown) case "${os}" in Darwin) os="darwin" ;; Linux) os="linux" ;; *) return 1 ;; esac case "${arch}" in x86_64|amd64) arch="x64" ;; arm64|aarch64) arch="arm64" ;; *) return 1 ;; esac echo "${os}-${arch}" } archive_extension_for_target() { case "$1" in darwin-*|linux-*) echo "tar.gz" ;; *) return 1 ;; esac } release_version_path() { if [[ "${VERSION}" == "latest" ]]; then echo "latest" return 0 fi case "${VERSION}" in v*) echo "${VERSION}" ;; *) echo "v${VERSION}" ;; esac } # When a shadowing 'qwen' is detected, append a PATH prepend to the user's # shell rc file at the very end. Putting it at the END means our prepend runs # AFTER any earlier PATH munging in the rc file (e.g., other tools' shell # init), so our installed_bin wins. Idempotent via a marker comment. maybe_update_shell_path() { local install_bin_dir="$1" [[ "${NO_MODIFY_PATH:-0}" == "1" ]] && return 0 [[ -z "${install_bin_dir}" ]] && return 0 [[ -z "${HOME:-}" ]] && return 0 local rc_file="" case "${SHELL:-}" in */zsh) rc_file="${HOME}/.zshrc" ;; */bash) if [[ -f "${HOME}/.bashrc" ]]; then rc_file="${HOME}/.bashrc" elif [[ -f "${HOME}/.bash_profile" ]]; then rc_file="${HOME}/.bash_profile" else rc_file="${HOME}/.bashrc" fi ;; */fish) rc_file="${HOME}/.config/fish/config.fish" ;; *) log_warning "Unsupported shell for automatic PATH update: ${SHELL:-unknown}. Add ${install_bin_dir} to PATH manually." return 0 ;; esac [[ -z "${rc_file}" ]] && return 0 local begin_marker="# Qwen Code PATH block begin" local end_marker="# Qwen Code PATH block end" local quoted_install_bin_dir quoted_install_bin_dir=$(shell_quote "${install_bin_dir}") local export_line if [[ "${rc_file}" == *config.fish ]]; then export_line="set -gx PATH ${quoted_install_bin_dir} \$PATH" else export_line="export PATH=${quoted_install_bin_dir}:\$PATH" fi if [[ -f "${rc_file}" ]] && grep -qxF "${export_line}" "${rc_file}" 2>/dev/null; then log_info "PATH update for ${install_bin_dir} already present in ${rc_file} (skipping)." return 0 fi mkdir -p "$(dirname "${rc_file}")" 2>/dev/null || true { echo "" echo "${begin_marker}" echo "${export_line}" echo "${end_marker}" } >> "${rc_file}" || { log_warning "Could not write PATH update to ${rc_file}." return 0 } log_success "Appended PATH prepend to ${rc_file}" log_info "Open a new terminal, or run: source ${rc_file}" } github_base_url_for_version() { local version_path="$1" local github_repo="${QWEN_INSTALL_GITHUB_REPO:-QwenLM/qwen-code}" if [[ "${version_path}" == "latest" ]]; then echo "https://github.com/${github_repo}/releases/latest/download" else echo "https://github.com/${github_repo}/releases/download/${version_path}" fi } aliyun_base_url_for_version() { local version_path="$1" echo "https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/releases/qwen-code/${version_path}" } aliyun_latest_version_url() { echo "https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/releases/qwen-code/latest/VERSION" } normalize_version_path_value() { local raw_version="$1" local version_path raw_version=$(printf '%s' "${raw_version}" | tr -d '\r' | awk 'NF { print $1; exit }') if [[ -z "${raw_version}" ]]; then return 1 fi case "${raw_version}" in v*) version_path="${raw_version}" ;; *) version_path="v${raw_version}" ;; esac if [[ "${version_path}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][A-Za-z0-9]+)*$ ]]; then echo "${version_path}" return 0 fi return 1 } download_text() { local url="$1" if command_exists curl; then curl -fsSL --retry 2 --connect-timeout 10 --max-time 30 "${url}" return $? fi if command_exists wget; then local wget_args=(--tries=3 --timeout=10) if wget --help 2>&1 | grep -q -- '--read-timeout'; then wget_args+=(--read-timeout=30) fi wget -q "${wget_args[@]}" -O - "${url}" return $? fi log_error "curl or wget is required to resolve the standalone release version." return 1 } resolve_aliyun_version_path() { local version_path="$1" if [[ "${version_path}" != "latest" ]]; then echo "${version_path}" return 0 fi local latest_url latest_url=$(aliyun_latest_version_url) local latest_version if ! latest_version=$(download_text "${latest_url}"); then log_warning "Failed to resolve Aliyun latest VERSION pointer." >&2 return 1 fi local resolved_version_path if ! resolved_version_path=$(normalize_version_path_value "${latest_version}"); then log_error "Aliyun latest VERSION pointer is not a valid semver value." return 1 fi log_info "Resolved Aliyun latest to ${resolved_version_path}." >&2 echo "${resolved_version_path}" } # Probe a URL with a HEAD request first, then fall back to a 1-byte ranged GET # for object stores or CDNs that reject HEAD while still serving the object. probe_url_available() { local url="$1" local timeout="${2:-30}" if command_exists curl; then if curl -fsIL --retry 1 --connect-timeout 10 --max-time "${timeout}" "${url}" >/dev/null 2>&1; then return 0 fi curl -fsL --retry 1 --connect-timeout 10 --max-time "${timeout}" \ --range 0-0 -o /dev/null "${url}" >/dev/null 2>&1 return $? fi if command_exists wget; then local wget_args=(--tries=2 --timeout=10) if wget --help 2>&1 | grep -q -- '--read-timeout'; then wget_args+=(--read-timeout="${timeout}") fi if wget -q --spider "${wget_args[@]}" "${url}" >/dev/null 2>&1; then return 0 fi wget -q "${wget_args[@]}" --header='Range: bytes=0-0' -O /dev/null "${url}" >/dev/null 2>&1 return $? fi return 1 } # Race two availability probes; print "aliyun" or "github" based on which # mirror's SHA256SUMS responds first, or "timeout" if neither responds before # the deadline. Caller decides what to do with "timeout" (currently: log it and # fall back to github). race_mirror_head() { local timeout="${1:-2}" local gh_url="$2" local oss_url="$3" local tmpdir if ! tmpdir=$(mktemp -d -t qwen-mirror.XXXXXX 2>/dev/null); then # Refuse to fall back to a predictable PID-based path; a local attacker # could pre-create it to influence mirror selection. echo "mirror probe: mktemp failed" >&2 echo "github" return 0 fi register_temp_dir "${tmpdir}" (probe_url_available "${oss_url}" "${timeout}" && : > "${tmpdir}/aliyun") & local oss_pid=$! (probe_url_available "${gh_url}" "${timeout}" && : > "${tmpdir}/github") & local gh_pid=$! local winner="" local elapsed=0 local max=$((timeout * 10 + 5)) while [[ -z "${winner}" && "${elapsed}" -lt "${max}" ]]; do # Probe OSS first to break ties in favor of the closer mirror for CN users. [[ -e "${tmpdir}/aliyun" ]] && winner="aliyun" && break [[ -e "${tmpdir}/github" ]] && winner="github" && break sleep 0.1 elapsed=$((elapsed + 1)) done kill "${oss_pid}" "${gh_pid}" 2>/dev/null || true wait "${oss_pid}" "${gh_pid}" 2>/dev/null || true rm -rf "${tmpdir}" 2>/dev/null || true echo "${winner:-timeout}" } standalone_base_url() { if [[ -n "${BASE_URL}" ]]; then echo "${BASE_URL%/}" return 0 fi local version_path version_path=$(release_version_path) if [[ "${MIRROR}" == "auto" ]]; then local gh_head oss_head selected gh_head="$(github_base_url_for_version "${version_path}")/SHA256SUMS" if [[ "${version_path}" == "latest" ]]; then oss_head="$(aliyun_latest_version_url)" else oss_head="$(aliyun_base_url_for_version "${version_path}")/SHA256SUMS" fi selected=$(race_mirror_head 2 "${gh_head}" "${oss_head}") if [[ "${selected}" == "timeout" ]]; then log_info "Mirror auto-selection timed out; defaulting to github." >&2 selected="github" else log_info "Mirror auto-selected via HEAD probe: ${selected}" >&2 fi MIRROR="${selected}" fi if [[ "${MIRROR}" == "aliyun" ]]; then if ! version_path=$(resolve_aliyun_version_path "${version_path}"); then return 1 fi aliyun_base_url_for_version "${version_path}" return 0 fi github_base_url_for_version "${version_path}" } download_file() { local url="$1" local destination="$2" if command_exists curl; then curl -fL --retry 2 --connect-timeout 15 --max-time 300 --progress-bar "${url}" -o "${destination}" return $? fi if command_exists wget; then local wget_args=(--tries=3 --timeout=15) if wget --help 2>&1 | grep -q -- '--read-timeout'; then wget_args+=(--read-timeout=300) fi if wget --help 2>&1 | grep -q -- '--progress'; then wget --progress=bar:force:noscroll "${wget_args[@]}" "${url}" -O "${destination}" || return 1 else wget "${wget_args[@]}" "${url}" -O "${destination}" || return 1 fi return $? fi log_error "curl or wget is required to download the standalone archive." return 1 } url_exists() { local url="$1" probe_url_available "${url}" 30 } sha256_file() { local file_path="$1" if command_exists sha256sum; then sha256sum "${file_path}" | awk '{print $1}' return 0 fi if command_exists shasum; then shasum -a 256 "${file_path}" | awk '{print $1}' return 0 fi return 1 } verify_checksum() { local archive_path="$1" local checksum_source="$2" local archive_name="$3" local checksum_file="${checksum_source}" local temp_checksum="" if [[ -z "${checksum_file}" ]]; then checksum_file="$(dirname "${archive_path}")/SHA256SUMS" elif [[ "${checksum_file}" == http://* || "${checksum_file}" == https://* ]]; then temp_checksum="$(mktemp)" if ! download_file "${checksum_file}" "${temp_checksum}"; then rm -f "${temp_checksum}" log_error "Could not download SHA256SUMS for checksum verification." return 1 fi checksum_file="${temp_checksum}" fi if [[ ! -f "${checksum_file}" ]]; then rm -f "${temp_checksum}" log_error "SHA256SUMS not found at ${checksum_file}; cannot verify archive." return 1 fi local expected expected=$(awk -v archive_name="${archive_name}" ' { name = $2 sub(/^\*/, "", name) if (name == archive_name) { print $1 exit } } ' "${checksum_file}") if [[ -z "${expected}" ]]; then rm -f "${temp_checksum}" log_error "Checksum entry for ${archive_name} not found." return 1 fi local actual if ! actual=$(sha256_file "${archive_path}"); then rm -f "${temp_checksum}" log_error "No SHA-256 utility found; cannot verify archive." return 1 fi rm -f "${temp_checksum}" if [[ "${expected}" != "${actual}" ]]; then log_error "Checksum mismatch for ${archive_name}: expected ${expected}, got ${actual}." return 1 fi log_success "Checksum verified for ${archive_name}." } validate_archive_entry_path() { local entry="$1" entry="${entry//\\//}" while [[ "${entry}" == ./* ]]; do entry="${entry#./}" done # Reject entries containing CR/LF so a `..\r` or `..\n` entry cannot # bypass the literal `..` glob below. case "${entry}" in *$'\r'*|*$'\n'*) log_error "Archive contains unsafe path with control character: ${entry}" return 1 ;; esac case "${entry}" in ""|/*|..|../*|*/..|*/../*) log_error "Archive contains unsafe path: ${entry:-}" return 1 ;; esac } validate_archive_contents() { local archive_path="$1" local entries local entry case "${archive_path}" in *.zip) if ! command_exists unzip; then log_error "unzip is required to inspect ${archive_path}." return 1 fi if ! entries=$(unzip -Z1 "${archive_path}"); then log_error "Failed to inspect archive entries: ${archive_path}" return 1 fi ;; *.tar.gz|*.tgz|*.tar.xz) if ! entries=$(tar -tf "${archive_path}"); then log_error "Failed to inspect archive entries: ${archive_path}" return 1 fi ;; *) log_error "Unsupported archive format: ${archive_path}" return 1 ;; esac while IFS= read -r entry; do validate_archive_entry_path "${entry}" || return 1 done <<< "${entries}" } extract_archive() { local archive_path="$1" local destination="$2" mkdir -p "${destination}" || return 1 validate_archive_contents "${archive_path}" || return 1 case "${archive_path}" in *.zip) if ! command_exists unzip; then log_error "unzip is required to extract ${archive_path}." return 1 fi unzip -q "${archive_path}" -d "${destination}" || return 1 ;; *.tar.gz|*.tgz) tar -xzf "${archive_path}" -C "${destination}" || return 1 ;; *.tar.xz) tar -xf "${archive_path}" -C "${destination}" || return 1 ;; *) log_error "Unsupported archive format: ${archive_path}" return 1 ;; esac local symlink_entry symlink_entry=$(find "${destination}" -type l -print | sed -n '1p') if [[ -n "${symlink_entry}" ]]; then log_error "Archive contains symlinks; refusing to install." return 1 fi } ensure_managed_install_dir() { local install_dir="$1" if [[ ! -e "${install_dir}" ]]; then return 0 fi if is_qwen_standalone_install_dir "${install_dir}"; then return 0 fi local backup="${install_dir}.backup.$(date +%Y%m%dT%H%M%S 2>/dev/null || date +%Y%m%d%H%M%S)" log_warning "${install_dir} exists but is not a Qwen Code standalone install." log_warning "Backing up to: ${backup}" if mv "${install_dir}" "${backup}"; then return 0 fi log_error "Failed to back up ${install_dir}. Move or remove it manually, then rerun the installer." return 1 } restore_stale_install_backup() { local old_install_dir="$1" local current_install_dir="$2" if [[ -e "${current_install_dir}" || ! -e "${old_install_dir}" ]]; then return 0 fi log_warning "Found previous install backup without an active install: ${old_install_dir}" log_warning "Restoring backup to ${current_install_dir} before continuing." if mv "${old_install_dir}" "${current_install_dir}"; then return 0 fi log_error "Failed to restore previous install from ${old_install_dir}." return 1 } is_qwen_standalone_install_dir() { local install_dir="$1" local manifest_path="${install_dir}/manifest.json" [[ -f "${manifest_path}" ]] || return 1 # Manifest format is produced by writeManifest in create-standalone-package.js. # Keep these grep checks in sync if that JSON layout changes. grep -Eq '"name"[[:space:]]*:[[:space:]]*"@qwen-code/qwen-code"' "${manifest_path}" 2>/dev/null || return 1 grep -Eq '"target"[[:space:]]*:[[:space:]]*"(darwin|linux)-(arm64|x64)"' "${manifest_path}" 2>/dev/null || return 1 [[ -f "${install_dir}/bin/qwen" && ! -L "${install_dir}/bin/qwen" && -x "${install_dir}/bin/qwen" ]] || return 1 [[ -f "${install_dir}/node/bin/node" && ! -L "${install_dir}/node/bin/node" && -x "${install_dir}/node/bin/node" ]] || return 1 } write_unix_wrapper() { local wrapper_path="$1" local qwen_bin="$2" local quoted_qwen_bin quoted_qwen_bin=$(shell_quote "${qwen_bin}") if ! cat > "${wrapper_path}" </dev/null || echo "unknown") log_info "Existing Qwen Code detected: ${qwen_version}" if [[ "${VERSION}" == "latest" ]]; then log_info "Upgrading to the latest version." else log_info "Installing requested version ${VERSION}." fi fi local install_cmd=( npm install -g "${package_spec}" --registry "${NPM_REGISTRY}" ) log_info "Running: npm install -g ${package_spec} --registry ${NPM_REGISTRY}" if "${install_cmd[@]}"; then log_success "Qwen Code installed successfully." create_source_json return 0 fi log_error "Failed to install Qwen Code." echo "" echo "This installer does not change your npm prefix or shell profile." echo "If the failure is a permission error, install Node.js with a user-owned" echo "Node version manager or fix your npm global package directory, then run:" echo " npm install -g ${package_spec} --registry ${NPM_REGISTRY}" return 1 } print_final_instructions() { local install_bin_dir="${1:-}" local install_dir="${2:-}" local install_method="${3:-standalone}" local installed_bin="" local quoted_install_bin_dir="" local standalone_uninstall_url="https://qwen-code-assets.oss-cn-hangzhou.aliyuncs.com/installation/uninstall-qwen-standalone.sh" if [[ -n "${install_bin_dir}" ]]; then installed_bin="${install_bin_dir}/qwen" quoted_install_bin_dir=$(shell_quote "${install_bin_dir}") fi # PRE_INSTALL_QWENS was captured by main() BEFORE the install ran # (newline-separated list of every qwen binary found on disk). Filter out # the one we just installed; whatever remains may shadow this install. local other_qwens="" if [[ -n "${PRE_INSTALL_QWENS:-}" ]]; then local saved_ifs="${IFS}" IFS=$'\n' local path for path in ${PRE_INSTALL_QWENS}; do [[ -z "${path}" ]] && continue [[ -n "${installed_bin}" && "${path}" == "${installed_bin}" ]] && continue if [[ -z "${other_qwens}" ]]; then other_qwens="${path}" else other_qwens="${other_qwens}"$'\n'"${path}" fi done IFS="${saved_ifs}" fi if [[ -n "${install_bin_dir}" ]]; then export PATH="${install_bin_dir}:${PATH}" fi echo "" local installed_version="unknown" if [[ -n "${installed_bin}" && -x "${installed_bin}" ]]; then installed_version=$("${installed_bin}" --version 2>/dev/null || echo "unknown") elif command_exists qwen; then installed_version=$(qwen --version 2>/dev/null || echo "unknown") fi echo "QWEN CODE" echo "" echo "Qwen Code ${installed_version} installed successfully." echo "" echo "To start:" echo " cd " echo " qwen" if [[ -n "${install_dir}" ]]; then echo "" echo "Installed to:" echo " ${install_dir}" fi echo "" echo "Uninstall:" if [[ "${install_method}" == "npm" ]]; then echo " npm uninstall -g @qwen-code/qwen-code" elif [[ -n "${install_dir}" && -n "${install_bin_dir}" ]]; then echo " curl -fsSL ${standalone_uninstall_url} | QWEN_INSTALL_LIB_DIR=$(shell_quote "${install_dir}") QWEN_INSTALL_BIN_DIR=$(shell_quote "${install_bin_dir}") bash" else echo " curl -fsSL ${standalone_uninstall_url} | bash" fi if [[ -n "${install_bin_dir}" && "${NO_MODIFY_PATH:-0}" != "1" ]]; then maybe_update_shell_path "${install_bin_dir}" fi if [[ -n "${other_qwens}" ]]; then echo "" log_warning "Other 'qwen' executables exist on this system. Depending on your" log_warning "shell PATH order, one of these may run instead of the install above:" local saved_ifs="${IFS}" IFS=$'\n' local path for path in ${other_qwens}; do [[ -z "${path}" ]] && continue log_warning " ${path}" done IFS="${saved_ifs}" echo "" echo "To make this install take priority, restart your terminal." echo "Or invoke directly: ${installed_bin}" return 0 fi echo "(Open a new terminal for the PATH change to take effect.)" } main() { if [[ -z "${HOME:-}" ]]; then log_error "HOME is not set; cannot determine where to install Qwen Code." exit 1 fi # Discover all qwen executables on disk BEFORE we install, so the # just-installed binary doesn't pollute the search. We can't reliably # simulate the user's interactive shell PATH (some tools inject their # bin only under a tty), so we enumerate well-known per-tool bin # directories plus whatever bash inherited on PATH. PRE_INSTALL_QWENS=$( { IFS=: for dir in $PATH; do [[ -z "${dir}" ]] && continue [[ -x "${dir}/qwen" ]] && echo "${dir}/qwen" done for candidate in \ "${HOME}/.opencode/bin/qwen" \ "${HOME}/.bun/bin/qwen" \ "${HOME}/.cargo/bin/qwen" \ "${HOME}/.deno/bin/qwen" \ "${HOME}/.volta/bin/qwen" \ "${HOME}/.fnm/bin/qwen" \ "${HOME}/.local/bin/qwen" \ "${HOME}/Library/pnpm/qwen" \ "/usr/local/bin/qwen" \ "/opt/homebrew/bin/qwen"; do [[ -x "${candidate}" ]] && echo "${candidate}" done if command_exists npm; then local npm_prefix npm_prefix=$(npm prefix -g 2>/dev/null || true) if [[ -n "${npm_prefix}" && -x "${npm_prefix}/bin/qwen" ]]; then echo "${npm_prefix}/bin/qwen" fi fi } 2>/dev/null | sort -u ) export PRE_INSTALL_QWENS print_header case "${METHOD}" in standalone) install_standalone print_final_instructions "${INSTALL_BIN_DIR}" "${INSTALL_LIB_DIR}" "standalone" ;; npm) install_npm print_final_instructions "$(get_npm_global_bin)" "$(get_npm_global_root)" "npm" ;; detect) # Try the standalone archive first; fall back only when unavailable. if install_standalone; then print_final_instructions "${INSTALL_BIN_DIR}" "${INSTALL_LIB_DIR}" "standalone" else standalone_status=$? if [[ "${standalone_status}" -eq 2 ]]; then log_warning "Falling back to npm installation." if install_npm; then print_final_instructions "$(get_npm_global_bin)" "$(get_npm_global_root)" "npm" else log_warning "Standalone archive was unavailable before npm fallback; npm fallback also failed." log_warning "Retry with --method standalone to debug the standalone failure, or install Node.js 22+ and rerun --method npm." exit 1 fi else log_warning "Standalone install failed. Retry with --method npm to use npm, or --method standalone to debug the standalone failure." exit "${standalone_status}" fi fi ;; esac } main "$@"