Created
February 24, 2026 17:23
-
-
Save hiway/d350399d78bd82153095476db6f2a4ab to your computer and use it in GitHub Desktop.
Install GitHub Copilot CLI on FreeBSD ARM64 hosts (fix pty.node issue)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/sh | |
| # shellcheck shell=sh | |
| set -eu | |
| # Why: non-interactive SSH sessions may have a limited PATH (missing /usr/sbin | |
| # or /usr/local/bin), which would make `pkg`/`npm` look unavailable. | |
| PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}" | |
| export PATH | |
| # This script makes GitHub Copilot CLI usable on FreeBSD ARM64 hosts. | |
| # | |
| # Why this exists: | |
| # - Copilot CLI currently ships prebuilt `pty.node` binaries for common OS/arch pairs. | |
| # - FreeBSD ARM64 is not bundled, so Copilot may fail at runtime with: | |
| # "Cannot find module './prebuilds/freebsd-arm64/pty.node'" | |
| # - We fix this by building `node-pty` locally and placing its binary where | |
| # Copilot expects it. | |
| # | |
| # Idempotency goals: | |
| # - Safe to run multiple times. | |
| # - Installs only missing packages. | |
| # - Skips rebuild when Copilot already runs unless FORCE_REBUILD=1. | |
| COPILOT_NPM_SPEC="${COPILOT_NPM_SPEC:-@github/copilot}" | |
| FORCE_REBUILD="${FORCE_REBUILD:-0}" | |
| USE_PRERELEASE=0 | |
| log() { | |
| printf '[copilot-setup] %s\n' "$*" | |
| } | |
| fail() { | |
| printf '[copilot-setup] ERROR: %s\n' "$*" >&2 | |
| exit 1 | |
| } | |
| usage() { | |
| cat <<'EOF' | |
| Usage: setup-copilot-cli-freebsd-arm64.sh [--prerelease] [--help] | |
| Options: | |
| --prerelease Install @github/copilot prerelease when Copilot is not installed | |
| --help Show this help | |
| Environment: | |
| FORCE_REBUILD=1 Force rebuilding pty.node workaround | |
| COPILOT_NPM_SPEC Override npm package spec (default: @github/copilot) | |
| EOF | |
| } | |
| parse_args() { | |
| while [ "$#" -gt 0 ]; do | |
| case "$1" in | |
| --prerelease) | |
| USE_PRERELEASE=1 | |
| ;; | |
| --help|-h) | |
| usage | |
| exit 0 | |
| ;; | |
| *) | |
| fail "Unknown argument: $1 (use --help)" | |
| ;; | |
| esac | |
| shift | |
| done | |
| if [ "$USE_PRERELEASE" = "1" ]; then | |
| COPILOT_NPM_SPEC="@github/copilot@prerelease" | |
| fi | |
| } | |
| need_cmd() { | |
| command -v "$1" >/dev/null 2>&1 || fail "Missing required command: $1" | |
| } | |
| # Runs command as root when needed. | |
| # Why: FreeBSD hosts may use either `doas` or `sudo`. | |
| as_root() { | |
| if [ "$(id -u)" -eq 0 ]; then | |
| "$@" | |
| elif command -v doas >/dev/null 2>&1; then | |
| doas "$@" | |
| elif command -v sudo >/dev/null 2>&1; then | |
| sudo "$@" | |
| else | |
| fail "Need root privileges but neither doas nor sudo is available" | |
| fi | |
| } | |
| install_pkg_if_missing() { | |
| # Why: `pkg install` is noisy and unnecessary when package already exists. | |
| # This check keeps the script quick and repeatable. | |
| pkg_name="$1" | |
| if pkg info -e "$pkg_name" >/dev/null 2>&1; then | |
| log "Package already installed: $pkg_name" | |
| else | |
| log "Installing missing package: $pkg_name" | |
| as_root pkg install -y "$pkg_name" | |
| fi | |
| } | |
| ensure_copilot_installed() { | |
| # Why: some hosts may not have Copilot installed globally yet. | |
| if command -v copilot >/dev/null 2>&1; then | |
| log "Copilot CLI already present: $(command -v copilot)" | |
| else | |
| log "Installing Copilot CLI globally via npm ($COPILOT_NPM_SPEC)" | |
| as_root npm install -g "$COPILOT_NPM_SPEC" | |
| fi | |
| } | |
| copilot_works() { | |
| copilot --version >/dev/null 2>&1 | |
| } | |
| rebuild_freebsd_arm64_pty() { | |
| npm_root="" | |
| copilot_dir="" | |
| expected_bin="" | |
| tmp_dir="" | |
| npm_root="$(npm root -g)" | |
| copilot_dir="$npm_root/@github/copilot" | |
| expected_bin="$copilot_dir/prebuilds/freebsd-arm64/pty.node" | |
| [ -d "$copilot_dir" ] || fail "Copilot directory not found at: $copilot_dir" | |
| if [ "$FORCE_REBUILD" != "1" ] && [ -f "$expected_bin" ] && copilot_works; then | |
| log "FreeBSD ARM64 pty binary already present and Copilot works; skipping rebuild" | |
| return 0 | |
| fi | |
| log "Building node-pty from source for FreeBSD ARM64" | |
| tmp_dir="$(mktemp -d)" | |
| # Why temporary directory: avoids polluting current repo/host directories. | |
| ( | |
| cd "$tmp_dir" | |
| npm init -y >/dev/null 2>&1 | |
| # Why environment variable: asks npm/node-gyp to compile native module locally. | |
| # This avoids depending on unavailable prebuilt binaries for FreeBSD ARM64. | |
| npm_config_build_from_source=true npm install node-pty | |
| [ -f "node_modules/node-pty/build/Release/pty.node" ] || fail "Build succeeded but pty.node not found" | |
| as_root mkdir -p "$(dirname "$expected_bin")" | |
| as_root cp "node_modules/node-pty/build/Release/pty.node" "$expected_bin" | |
| ) | |
| rm -rf "$tmp_dir" | |
| log "Installed rebuilt pty.node to: $expected_bin" | |
| } | |
| main() { | |
| parse_args "$@" | |
| need_cmd uname | |
| need_cmd pkg | |
| need_cmd npm | |
| os="" | |
| arch="" | |
| os="$(uname -s | tr '[:upper:]' '[:lower:]')" | |
| arch="$(uname -m | tr '[:upper:]' '[:lower:]')" | |
| log "Detected platform: ${os}/${arch}" | |
| install_pkg_if_missing gmake | |
| install_pkg_if_missing python3 | |
| install_pkg_if_missing npm | |
| ensure_copilot_installed | |
| # Fast path: if Copilot works already and no forced rebuild requested, exit quickly. | |
| if [ "$FORCE_REBUILD" != "1" ] && copilot_works; then | |
| log "Copilot CLI already functional; nothing to do" | |
| copilot --version | |
| return 0 | |
| fi | |
| # Only apply the pty workaround on the platform that needs it. | |
| if [ "$os" = "freebsd" ] && [ "$arch" = "arm64" ]; then | |
| rebuild_freebsd_arm64_pty | |
| else | |
| log "Not freebsd/arm64; skipping node-pty workaround" | |
| fi | |
| if copilot_works; then | |
| log "Copilot CLI is now functional" | |
| copilot --version | |
| else | |
| fail "Copilot still fails after setup. Run with FORCE_REBUILD=1 and inspect output." | |
| fi | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment