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.
Install GitHub Copilot CLI on FreeBSD ARM64 hosts (fix pty.node issue)
#!/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