Skip to content

Instantly share code, notes, and snippets.

@hiway
Created February 24, 2026 17:23
Show Gist options
  • Select an option

  • Save hiway/d350399d78bd82153095476db6f2a4ab to your computer and use it in GitHub Desktop.

Select an option

Save hiway/d350399d78bd82153095476db6f2a4ab to your computer and use it in GitHub Desktop.

Revisions

  1. hiway created this gist Feb 24, 2026.
    198 changes: 198 additions & 0 deletions setup-copilot-cli-freebsd-arm64.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,198 @@
    #!/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 "$@"