Files

1729 lines
50 KiB
Bash

#!/bin/sh
PI_PACKAGE="@earendil-works/pi-coding-agent"
PI_CMD="pi"
PI_INSTALLER_API_BASE="${PI_INSTALLER_API_BASE:-https://pi.dev/api/installer/releases}"
PI_MANAGED_INSTALL_MARKER="managed-install.json"
# Pi publishes npm-shrinkwrap.json, so the explicit installer/reinstaller can
# bypass npm's release-age gate without reopening transitive dependency ranges.
PI_NPM_INSTALL_MIN_AGE_ARG="--min-release-age=4"
PI_ESC=$(printf '\033')
PI_CR=$(printf '\r')
PI_ETX=$(printf '\003')
readonly PI_PACKAGE PI_CMD PI_INSTALLER_API_BASE PI_MANAGED_INSTALL_MARKER PI_NPM_INSTALL_MIN_AGE_ARG PI_ESC PI_CR PI_ETX
pi_installer_main() {
set -eu
check_file="${TMPDIR:-/tmp}/pi-installer-checks.$$"
run_preflight_checks >"$check_file" &
check_pid=$!
pi_logo_animation
if wait "$check_pid"; then
check_status=0
else
check_status=$?
fi
printf '\033[1m Pi Installer\033[0m\n\033[2m There are many agent harnesses but this one is yours\033[0m\n\n'
if [ "$check_status" -eq 0 ]; then
cat "$check_file"
fi
rm -f "$check_file"
if [ "$check_status" -ne 0 ]; then
if ! install_node_npm_interactive; then
exit "$check_status"
fi
check_file="${TMPDIR:-/tmp}/pi-installer-checks.$$"
if run_preflight_checks >"$check_file"; then
check_status=0
else
check_status=$?
fi
cat "$check_file"
rm -f "$check_file"
if [ "$check_status" -ne 0 ]; then
exit "$check_status"
fi
fi
PI_EXISTING_PATH=$(command -v "$PI_CMD" 2>/dev/null || true)
export PI_EXISTING_PATH
if pi_managed_install_enabled; then
if ! ensure_managed_install_supported; then
exit 1
fi
PI_MANAGED_INSTALL_DIR=$(select_managed_install_dir "$PI_EXISTING_PATH")
PI_MANAGED_AGENT_DIR=${PI_MANAGED_INSTALL_DIR%/*}
PI_MANAGED_BIN_DIR=$(select_managed_path_bin_dir "$PI_MANAGED_AGENT_DIR" "$PI_MANAGED_INSTALL_DIR" "$PI_EXISTING_PATH")
PI_NPM_INSTALL_PREFIX=
export PI_MANAGED_INSTALL_DIR PI_MANAGED_AGENT_DIR PI_MANAGED_BIN_DIR
else
if ! PI_NPM_INSTALL_PREFIX=$(select_npm_install_prefix); then
exit 1
fi
fi
export PI_NPM_INSTALL_PREFIX
if pi_managed_install_enabled && managed_install_root_for_command "$PI_EXISTING_PATH" >/dev/null 2>&1; then
PI_NPM_UNINSTALL_PREFIX=
else
PI_NPM_UNINSTALL_PREFIX=$(select_npm_uninstall_prefix "$PI_EXISTING_PATH")
fi
export PI_NPM_UNINSTALL_PREFIX
choose_pi_action "$PI_EXISTING_PATH"
case "$PI_INSTALL_ACTION" in
uninstall)
uninstall_pi_package
printf '\nPi was uninstalled successfully.\n'
exit 0
;;
none)
exit 0
;;
esac
install_pi_package
if [ "$PI_INSTALL_ACTION" = reinstall ]; then
printf '\nPi was reinstalled successfully.\n'
else
printf '\nPi was installed successfully.\n'
fi
if pi_managed_install_enabled; then
printf '\nUpdate Pi later with: pi update\n'
fi
if installed_pi_is_first_on_path; then
printf '\nRun it with: pi\n'
if [ "${PI_NODE_INSTALLED_STANDALONE:-0}" = 1 ]; then
printf 'If pi is not found in your shell yet, add this to your shell profile:\n\n'
printf ' export PATH="%s:$PATH"\n' "$PI_STANDALONE_NODE_BIN"
fi
else
print_pi_not_on_path_message
fi
}
run_preflight_checks() {
status=0
if command -v node >/dev/null 2>&1; then
node_version=$(node --version)
if ! node -e 'const [maj,min,patch] = process.versions.node.split(".").map(Number); process.exit(maj > 22 || (maj === 22 && (min > 19 || (min === 19 && patch >= 0))) ? 0 : 1)' >/dev/null; then
printf 'error: Pi requires Node.js 22.19.0 or newer. Found %s.\n' "$node_version"
status=1
fi
else
printf 'error: Node.js 22.19.0 or newer is required to install Pi.\n'
status=1
fi
if ! command -v npm >/dev/null 2>&1; then
printf 'error: npm is required to install Pi.\n'
status=1
fi
if [ "$status" -ne 0 ]; then
printf '\n'
fi
return "$status"
}
install_node_npm_interactive() {
method=$(detect_node_install_method)
case "$method" in
homebrew) label="Homebrew" ;;
apt) label="apt" ;;
apk) label="apk" ;;
standalone) label="standalone Node.js" ;;
esac
if ! ( : <>/dev/tty ) 2>/dev/null; then
printf 'No terminal detected; install Node.js 22.19.0 or newer and npm, then run this installer again.\n'
return 1
fi
exec 3<>/dev/tty
printf 'Pi needs Node.js 22.19.0 or newer and npm. Install them now with %s? [Y/n] ' "$label" >&3
if ! IFS= read -r answer <&3; then
answer=
fi
exec 3>&-
case "$answer" in
n|N|no|NO) printf '\nInstall Node.js 22.19.0 or newer and npm, then run this installer again.\n'; return 1 ;;
*) ;;
esac
install_node_npm "$method" "$label"
}
detect_node_install_method() {
case "$(uname -s)" in
Darwin)
if command -v brew >/dev/null 2>&1; then
printf 'homebrew'
else
printf 'standalone'
fi
;;
Linux)
if command -v apt-cache >/dev/null 2>&1 && command -v apt-get >/dev/null 2>&1 && apt_node_candidate_is_new_enough; then
printf 'apt'
elif command -v apk >/dev/null 2>&1 && apk_node_candidate_is_new_enough; then
printf 'apk'
else
printf 'standalone'
fi
;;
*)
printf 'standalone'
;;
esac
}
apt_node_candidate_is_new_enough() {
version=$(apt-cache policy nodejs 2>/dev/null | awk '/Candidate:/ { print $2; exit }')
[ -n "$version" ] && [ "$version" != "(none)" ] && node_version_string_is_new_enough "$version"
}
apk_node_candidate_is_new_enough() {
version=$(apk search -x nodejs 2>/dev/null | awk -F- '/^nodejs-/ { print $2; exit }')
[ -n "$version" ] && node_version_string_is_new_enough "$version"
}
node_version_string_is_new_enough() {
version="${1#v}"
case "$version" in
[0-9]*) ;;
*) return 1 ;;
esac
version="${version%%[!0-9.]*}"
version_ifs=${IFS- }
IFS=.
set -- $version
IFS=$version_ifs
major="${1:-}"
minor="${2:-0}"
patch="${3:-0}"
case "$major" in ''|*[!0-9]*) return 1 ;; esac
case "$minor" in ''|*[!0-9]*) minor=0 ;; esac
case "$patch" in ''|*[!0-9]*) patch=0 ;; esac
[ "$major" -gt 22 ] && return 0
[ "$major" -eq 22 ] && [ "$minor" -gt 19 ] && return 0
[ "$major" -eq 22 ] && [ "$minor" -eq 19 ] && [ "$patch" -ge 0 ] && return 0
return 1
}
install_node_npm() {
method="$1"; label="$2"
if [ -t 1 ] && [ "${TERM:-}" != "dumb" ]; then
install_node_npm_with_progress "$method" "$label"
else
printf '\nInstalling Node.js and npm with %s...\n\n' "$label"
run_node_install_method "$method"
printf '\nNode.js and npm are installed.\n'
fi
if [ "$method" = standalone ]; then
load_standalone_node
PI_NODE_INSTALLED_STANDALONE=1
fi
hash -r
printf '\n'
}
install_node_npm_with_progress() {
method="$1"; label="$2"
log_file="${TMPDIR:-/tmp}/pi-installer-node.$$"
rm -f "$log_file"
: >"$log_file"
run_node_install_method "$method" >"$log_file" 2>&1 &
install_pid=$!
printf '\033[?25l'
animate_node_install "$log_file" "$label" &
progress_pid=$!
trap 'kill "$install_pid" 2>/dev/null || true; finish_install_progress "$progress_pid"; exit 130' INT TERM
if wait "$install_pid"; then
status=0
else
status=$?
fi
finish_install_progress "$progress_pid"
trap - INT TERM
if [ "$status" -ne 0 ]; then
printf '\033[31mNode.js installation failed.\033[0m\n\n'
cat "$log_file"
rm -f "$log_file"
return "$status"
fi
rm -f "$log_file"
if terminal_supports_unicode; then
printf ' \033[32m✓\033[0m Node.js and npm install complete\n'
else
printf ' \033[32mok\033[0m Node.js and npm install complete\n'
fi
}
run_node_install_method() {
case "$1" in
homebrew) install_node_with_homebrew ;;
apt) install_node_with_apt ;;
apk) install_node_with_apk ;;
standalone) install_node_standalone ;;
esac
}
install_node_with_homebrew() {
if brew list node >/dev/null 2>&1; then
brew upgrade node
else
brew install node
fi
}
install_node_with_apt() {
print_sudo_note
if [ "${EUID:-$(id -u)}" -eq 0 ]; then
apt-get update
apt-get install -y nodejs npm
else
sudo sh -c 'apt-get update && apt-get install -y nodejs npm'
fi
}
install_node_with_apk() {
print_sudo_note
run_with_sudo apk add --update-cache nodejs npm
}
install_node_standalone() {
node_platform=$(detect_node_binary_platform) || {
printf 'Unsupported operating system for automatic Node.js install: %s\n' "$(uname -s)"
return 1
}
node_arch=$(detect_node_binary_arch) || {
printf 'Unsupported CPU architecture for automatic Node.js install: %s\n' "$(uname -m)"
return 1
}
node_dist_base="https://nodejs.org/dist/latest-v22.x"
node_base_dir=$(node_standalone_base_dir)
node_tmp_dir="${TMPDIR:-/tmp}/pi-node.$$"
rm -rf "$node_tmp_dir"
mkdir -p "$node_tmp_dir" "$node_base_dir"
printf 'Resolving Node.js binary for %s-%s\n' "$node_platform" "$node_arch"
curl -fsSL "$node_dist_base/SHASUMS256.txt" -o "$node_tmp_dir/SHASUMS256.txt"
node_file=$(awk -v suffix="-$node_platform-$node_arch.tar.xz" '
index($2, "node-v") == 1 && length($2) >= length(suffix) && substr($2, length($2) - length(suffix) + 1) == suffix { print $2; exit }
' "$node_tmp_dir/SHASUMS256.txt")
if [ -z "$node_file" ]; then
printf 'No Node.js binary is available for %s-%s.\n' "$node_platform" "$node_arch"
rm -rf "$node_tmp_dir"
return 1
fi
printf 'Downloading Node.js %s\n' "${node_file%.tar.xz}"
curl -fsSL "$node_dist_base/$node_file" -o "$node_tmp_dir/$node_file"
verify_node_standalone_download "$node_tmp_dir" "$node_file"
ensure_node_standalone_extract_tools "$node_platform"
node_dir="$node_base_dir/${node_file%.tar.xz}"
rm -rf "$node_dir"
printf 'Extracting Node.js to %s\n' "$node_dir"
tar -xf "$node_tmp_dir/$node_file" -C "$node_base_dir"
rm -f "$node_base_dir/current"
ln -s "$node_dir" "$node_base_dir/current"
rm -rf "$node_tmp_dir"
printf 'Node.js installed at %s\n' "$node_dir"
}
verify_node_standalone_download() {
checksum_dir="$1"
checksum_file_name="$2"
awk -v file="$checksum_file_name" '$2 == file { print }' "$checksum_dir/SHASUMS256.txt" > "$checksum_dir/SHASUMS256.selected"
if command -v sha256sum >/dev/null 2>&1; then
printf 'Verifying Node.js download\n'
(cd "$checksum_dir" && sha256sum -c SHASUMS256.selected)
elif command -v shasum >/dev/null 2>&1; then
printf 'Verifying Node.js download\n'
(cd "$checksum_dir" && shasum -a 256 -c SHASUMS256.selected)
fi
}
ensure_node_standalone_extract_tools() {
extract_platform="$1"
if [ "$extract_platform" = linux ] && ! command -v xz >/dev/null 2>&1; then
printf 'Installing xz-utils for Node.js archive extraction\n'
print_sudo_note
if command -v apt-get >/dev/null 2>&1; then
run_with_sudo apt-get update
run_with_sudo apt-get install -y xz-utils
elif command -v apk >/dev/null 2>&1; then
run_with_sudo apk add --update-cache xz
else
printf 'xz is required to extract Node.js. Install xz and run this installer again.\n'
return 1
fi
fi
}
load_standalone_node() {
PI_STANDALONE_NODE_BIN="$(node_standalone_base_dir)/current/bin"
PATH="$PI_STANDALONE_NODE_BIN:$PATH"
export PI_STANDALONE_NODE_BIN PATH
}
node_standalone_base_dir() {
if [ -n "${XDG_DATA_HOME:-}" ]; then
printf '%s/pi-node' "$XDG_DATA_HOME"
else
printf '%s/.local/share/pi-node' "$HOME"
fi
}
detect_node_binary_platform() {
case "$(uname -s)" in
Darwin) printf 'darwin' ;;
Linux) printf 'linux' ;;
*) return 1 ;;
esac
}
detect_node_binary_arch() {
case "$(uname -m)" in
x86_64|amd64) printf 'x64' ;;
arm64|aarch64) printf 'arm64' ;;
armv7l) printf 'armv7l' ;;
ppc64le) printf 'ppc64le' ;;
s390x) printf 's390x' ;;
*) return 1 ;;
esac
}
print_sudo_note() {
if [ "${EUID:-$(id -u)}" -ne 0 ]; then
printf 'This may ask for your sudo password.\n\n'
fi
}
run_with_sudo() {
if [ "${EUID:-$(id -u)}" -eq 0 ]; then
"$@"
else
sudo "$@"
fi
}
select_npm_install_prefix() {
npm_prefix=$(npm_global_prefix)
if [ -n "$npm_prefix" ] && npm_prefix_supports_global_install "$npm_prefix"; then
return 0
fi
if existing_global_pi_blocks_user_local_install "$npm_prefix"; then
print_existing_global_pi_not_writable_message "$npm_prefix"
return 1
fi
printf '%s/.local' "$HOME"
}
select_npm_uninstall_prefix() {
existing_pi_path="$1"
[ -n "$existing_pi_path" ] || return 0
npm_prefix=$(npm_global_prefix)
if [ -n "$npm_prefix" ] && [ "$existing_pi_path" = "$npm_prefix/bin/$PI_CMD" ]; then
return 0
fi
if [ -n "${PI_NPM_INSTALL_PREFIX:-}" ] && [ "$existing_pi_path" = "$PI_NPM_INSTALL_PREFIX/bin/$PI_CMD" ]; then
printf '%s' "$PI_NPM_INSTALL_PREFIX"
return 0
fi
pi_bin_suffix="/bin/$PI_CMD"
case "$existing_pi_path" in
*"$pi_bin_suffix") printf '%s' "${existing_pi_path%$pi_bin_suffix}" ;;
esac
}
npm_global_prefix() {
npm prefix -g 2>/dev/null || npm config get prefix 2>/dev/null
}
npm_prefix_supports_global_install() {
prefix="$1"
path_is_writable_or_creatable "$prefix/lib/node_modules" && path_is_writable_or_creatable "$prefix/bin"
}
existing_global_pi_blocks_user_local_install() {
npm_prefix="$1"
[ -n "$npm_prefix" ] || return 1
[ -e "$npm_prefix/bin/$PI_CMD" ]
}
print_existing_global_pi_not_writable_message() {
npm_prefix="$1"
existing_pi_path="$npm_prefix/bin/$PI_CMD"
printf "npm's global directory is not writable: %s\n" "$npm_prefix" >&2
printf 'Pi is already installed at: %s\n\n' "$existing_pi_path" >&2
printf 'Installing another copy under %s/.local could leave your shell using the old global pi, so this installer stopped.\n\n' "$HOME" >&2
printf 'Update or remove the existing global install first. If it was installed with npm, you can run:\n\n' >&2
printf ' sudo npm install -g --ignore-scripts %s %s\n\n' "$PI_NPM_INSTALL_MIN_AGE_ARG" "$PI_PACKAGE" >&2
printf 'or uninstall it first with:\n\n' >&2
printf ' sudo npm uninstall -g %s\n\n' "$PI_PACKAGE" >&2
printf 'Then run this installer again.\n' >&2
}
path_is_writable_or_creatable() {
check_path="$1"
while [ ! -e "$check_path" ]; do
parent=${check_path%/*}
if [ -z "$parent" ] || [ "$parent" = "$check_path" ]; then
return 1
fi
check_path="$parent"
done
[ -d "$check_path" ] && [ -w "$check_path" ]
}
pi_install_bin_dir() {
if pi_managed_install_enabled; then
printf '%s' "$PI_MANAGED_BIN_DIR"
elif [ -n "${PI_NPM_INSTALL_PREFIX:-}" ]; then
printf '%s/bin' "$PI_NPM_INSTALL_PREFIX"
else
npm_prefix=$(npm_global_prefix)
if [ -n "$npm_prefix" ]; then
printf '%s/bin' "$npm_prefix"
fi
fi
}
pi_installed_path() {
pi_bin_dir=$(pi_install_bin_dir)
if [ -n "$pi_bin_dir" ]; then
printf '%s/%s' "$pi_bin_dir" "$PI_CMD"
fi
}
installed_pi_is_first_on_path() {
installed_pi_path=$(pi_installed_path)
[ -n "$installed_pi_path" ] || return 1
active_pi_path=$(command -v "$PI_CMD" 2>/dev/null) || return 1
[ "$active_pi_path" = "$installed_pi_path" ]
}
shell_config_file() {
current_shell=$(basename "${SHELL:-sh}")
case "$current_shell" in
fish) printf '%s/.config/fish/config.fish' "$HOME" ;;
zsh) printf '%s/.zshrc' "${ZDOTDIR:-$HOME}" ;;
bash)
if [ -f "$HOME/.bashrc" ]; then
printf '%s/.bashrc' "$HOME"
else
printf '%s/.profile' "$HOME"
fi
;;
*) printf '%s/.profile' "$HOME" ;;
esac
}
path_update_command() {
bin_dir="$1"
current_shell=$(basename "${SHELL:-sh}")
if [ "$bin_dir" = "$HOME/.local/bin" ]; then
bin_expr='$HOME/.local/bin'
else
bin_expr="$bin_dir"
fi
case "$current_shell" in
fish) printf 'fish_add_path "%s"' "$bin_expr" ;;
*) printf 'export PATH="%s:$PATH"' "$bin_expr" ;;
esac
}
config_file_mentions_path() {
config_file="$1"
command="$2"
[ -f "$config_file" ] || return 1
grep -Fxq "$command" "$config_file"
}
prompt_add_path_to_profile() {
bin_dir="$1"
if ! ( : <>/dev/tty ) 2>/dev/null; then
return 1
fi
config_file=$(shell_config_file)
command=$(path_update_command "$bin_dir")
if config_file_mentions_path "$config_file" "$command"; then
printf 'A PATH update for %s already exists in %s.\n' "$bin_dir" "$config_file"
return 0
fi
exec 3<>/dev/tty
printf 'Add %s to your PATH in %s now? [Y/n] ' "$bin_dir" "$config_file" >&3
if ! IFS= read -r answer <&3; then
answer=
fi
exec 3>&-
case "$answer" in
n|N|no|NO) return 1 ;;
*) ;;
esac
mkdir -p "${config_file%/*}"
touch "$config_file"
printf '\n# Pi\n%s\n' "$command" >> "$config_file"
printf 'Added %s to %s.\n' "$bin_dir" "$config_file"
}
print_pi_not_on_path_message() {
pi_bin_dir=$(pi_install_bin_dir)
active_pi_path=$(command -v "$PI_CMD" 2>/dev/null || true)
printf 'Pi was installed, but your shell is not using that install yet.\n'
if [ -n "$active_pi_path" ]; then
printf 'Your shell currently resolves pi to: %s\n' "$active_pi_path"
fi
if [ -n "$pi_bin_dir" ]; then
prompt_add_path_to_profile "$pi_bin_dir" || true
command=$(path_update_command "$pi_bin_dir")
printf 'Restart your shell or run:\n\n'
printf ' %s\n\n' "$command"
printf 'Then run: pi\n'
else
printf "Check npm's global prefix with:\n\n"
printf ' npm prefix -g\n\n'
printf 'Then add its bin directory to your shell PATH.\n'
fi
}
choose_pi_action() {
existing_pi_path="$1"
if ! ( : <>/dev/tty ) 2>/dev/null; then
print_pi_action_menu "$existing_pi_path"
printf 'No terminal detected; continuing without confirmation.\n'
if [ -n "$existing_pi_path" ]; then
PI_INSTALL_ACTION=reinstall
else
PI_INSTALL_ACTION=install
fi
print_pi_action_selection "$PI_INSTALL_ACTION"
return 0
fi
exec 3<>/dev/tty
trap 'exec 3>&-; trap - INT TERM; exit 130' INT TERM
print_pi_action_menu "$existing_pi_path" >&3
while :; do
key=$(read_tty_key)
case "$key" in
""|" "|"$PI_CR")
if [ -n "$existing_pi_path" ]; then
PI_INSTALL_ACTION=reinstall
else
PI_INSTALL_ACTION=install
fi
break
;;
y|Y)
if [ -n "$existing_pi_path" ]; then
PI_INSTALL_ACTION=reinstall
else
PI_INSTALL_ACTION=install
fi
break
;;
u|U)
if [ -n "$existing_pi_path" ]; then
PI_INSTALL_ACTION=uninstall
break
fi
;;
"$PI_ETX")
exit 130
;;
n|N|"$PI_ESC")
PI_INSTALL_ACTION=none
break
;;
esac
printf 'Please choose one of the listed keys.\n' >&3
done
print_pi_action_selection "$PI_INSTALL_ACTION" >&3
exec 3>&-
trap - INT TERM
}
print_pi_action_menu() {
existing_pi_path="$1"
reset=
dim=
bold=
cyan=
green=
red=
if [ -t 1 ] && [ "${TERM:-}" != "dumb" ]; then
reset="${PI_ESC}[0m"
dim="${PI_ESC}[2m"
bold="${PI_ESC}[1m"
cyan="${PI_ESC}[36m"
green="${PI_ESC}[32m"
red="${PI_ESC}[31m"
fi
if [ -n "$existing_pi_path" ]; then
printf '%sPi is already installed at:%s\n\n' "$bold" "$reset"
printf ' %s\n\n' "$existing_pi_path"
fi
if [ -n "${PI_NPM_INSTALL_PREFIX:-}" ]; then
printf "npm's global directory is not writable; Pi will be installed under %s.\n\n" "$PI_NPM_INSTALL_PREFIX"
fi
if pi_managed_install_enabled; then
if [ -n "$existing_pi_path" ]; then
printf '%sReinstallation:%s\n\n ' "$bold" "$reset"
else
printf '%sInstallation:%s\n\n ' "$bold" "$reset"
fi
elif [ -n "$existing_pi_path" ]; then
printf '%sReinstall command:%s\n\n ' "$bold" "$reset"
else
printf '%sInstall command:%s\n\n ' "$bold" "$reset"
fi
print_pi_install_command
printf '\n\n'
printf '%sChoose an action:%s\n\n' "$bold" "$reset"
if [ -n "$existing_pi_path" ]; then
printf ' %s%-4s%s %sReinstall Pi%s %s(default)%s\n' "$cyan" 'y' "$reset" "$green" "$reset" "$dim" "$reset"
printf ' %s%-4s%s %sUninstall Pi%s\n' "$cyan" 'u' "$reset" "$red" "$reset"
else
printf ' %s%-4s%s %sInstall Pi%s %s(default)%s\n' "$cyan" 'y' "$reset" "$green" "$reset" "$dim" "$reset"
fi
printf ' %s%-4s%s %sDo nothing%s\n' "$cyan" 'n' "$reset" "$dim" "$reset"
}
print_pi_action_selection() {
case "$1" in
install) message="Will install Pi." ;;
reinstall) message="Will reinstall Pi." ;;
uninstall) message="Will uninstall Pi." ;;
none) message="Chose to do nothing. Exiting." ;;
esac
printf '\n%s\n\n' "$message"
}
restore_tty_state() {
tty_state="$1"
[ -n "$tty_state" ] || return 0
stty "$tty_state" < /dev/tty 2>/dev/null || true
}
read_tty_key() {
old_tty_state=$(stty -g < /dev/tty 2>/dev/null || true)
trap 'restore_tty_state "$old_tty_state"; trap - INT TERM; exit 130' INT TERM
stty -icanon -echo min 1 time 0 < /dev/tty 2>/dev/null || true
if ! key=$(dd bs=1 count=1 2>/dev/null < /dev/tty); then
key=
fi
restore_tty_state "$old_tty_state"
trap - INT TERM
printf '%s' "$key"
}
print_pi_install_command() {
if pi_managed_install_enabled; then
printf 'Using experimental self managed installation\n Pi will install to %s/%s' "$PI_MANAGED_BIN_DIR" "$PI_CMD"
elif [ -n "${PI_NPM_INSTALL_PREFIX:-}" ]; then
printf 'npm install -g --ignore-scripts %s --prefix %s %s' "$PI_NPM_INSTALL_MIN_AGE_ARG" "$PI_NPM_INSTALL_PREFIX" "$PI_PACKAGE"
else
printf 'npm install -g --ignore-scripts %s %s' "$PI_NPM_INSTALL_MIN_AGE_ARG" "$PI_PACKAGE"
fi
}
pi_managed_install_enabled() {
[ "${PI_EXPERIMENTAL:-}" = 1 ]
}
ensure_managed_install_supported() {
case "$(uname -s)" in
Darwin|Linux) return 0 ;;
*)
printf 'Experimental managed Pi installs currently support macOS and Linux only.\n' >&2
return 1
;;
esac
}
managed_install_marker_is_valid() {
managed_root="$1"
marker_path="$managed_root/$PI_MANAGED_INSTALL_MARKER"
[ -f "$marker_path" ] || return 1
node - "$marker_path" <<'NODE' >/dev/null 2>&1
const fs = require("node:fs");
const marker = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
if (
marker.kind !== "pi-managed-install" ||
marker.schemaVersion !== 1 ||
marker.layout !== "releases-v1"
) {
process.exit(1);
}
NODE
}
managed_install_root_for_command() {
managed_command_path="$1"
[ -n "$managed_command_path" ] || return 1
[ "${managed_command_path##*/}" = "$PI_CMD" ] || return 1
managed_candidate=${managed_command_path%/*}
if managed_install_marker_is_valid "$managed_candidate"; then
printf '%s' "$managed_candidate"
return 0
fi
if ! managed_resolved_command=$(node - "$managed_command_path" <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
let resolved = path.resolve(process.argv[2]);
while (fs.lstatSync(resolved).isSymbolicLink()) {
const target = fs.readlinkSync(resolved);
resolved = path.resolve(path.dirname(resolved), target);
}
console.log(resolved);
NODE
); then
return 1
fi
[ "${managed_resolved_command##*/}" = "$PI_CMD" ] || return 1
managed_resolved_bin=${managed_resolved_command%/*}
[ "${managed_resolved_bin##*/}" = bin ] || return 1
managed_candidate=${managed_resolved_bin%/*}/install
if managed_install_marker_is_valid "$managed_candidate"; then
printf '%s' "$managed_candidate"
return 0
fi
return 1
}
select_managed_install_dir() {
existing_pi_path="$1"
if [ -n "${PI_MANAGED_INSTALL_ROOT:-}" ]; then
managed_candidate=${PI_MANAGED_INSTALL_ROOT%/}
if managed_install_marker_is_valid "$managed_candidate"; then
printf '%s' "$managed_candidate"
return 0
fi
fi
if managed_candidate=$(managed_install_root_for_command "$existing_pi_path"); then
printf '%s' "$managed_candidate"
return 0
fi
managed_agent_dir=${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}
printf '%s/install' "${managed_agent_dir%/}"
}
select_managed_path_bin_dir() {
managed_agent_dir="$1"
managed_root="$2"
existing_pi_path="$3"
if [ -n "$existing_pi_path" ] && managed_existing_root=$(managed_install_root_for_command "$existing_pi_path") && [ "$managed_existing_root" = "$managed_root" ]; then
managed_existing_bin=${existing_pi_path%/*}
if [ "$managed_existing_bin" != "$managed_root" ]; then
printf '%s' "$managed_existing_bin"
return 0
fi
fi
managed_path_ifs=${IFS- }
IFS=:
for managed_path_dir in ${PATH:-}; do
IFS=$managed_path_ifs
managed_path_dir=${managed_path_dir%/}
case "$managed_path_dir" in
"$managed_agent_dir/bin"|"$HOME/.local/bin"|"$HOME/bin"|"$HOME/.bin"|"$HOME/local/bin")
if path_is_writable_or_creatable "$managed_path_dir"; then
printf '%s' "$managed_path_dir"
return 0
fi
;;
esac
IFS=:
done
IFS=$managed_path_ifs
printf '%s/bin' "$managed_agent_dir"
}
install_pi_package() {
if [ -t 1 ] && [ "${TERM:-}" != "dumb" ]; then
install_pi_package_with_progress
else
printf 'Installing Pi...\n\n'
run_pi_install error
fi
}
run_pi_install() {
npm_loglevel="$1"
if pi_managed_install_enabled; then
run_managed_install_pi "$npm_loglevel"
else
run_npm_install_pi "$npm_loglevel"
fi
}
run_npm_install_pi() {
npm_loglevel="$1"
if [ -n "${PI_NPM_INSTALL_PREFIX:-}" ]; then
npm install -g --ignore-scripts "$PI_NPM_INSTALL_MIN_AGE_ARG" --prefix "$PI_NPM_INSTALL_PREFIX" --no-fund --no-audit "--loglevel=$npm_loglevel" --progress=false "$PI_PACKAGE"
else
npm install -g --ignore-scripts "$PI_NPM_INSTALL_MIN_AGE_ARG" --no-fund --no-audit "--loglevel=$npm_loglevel" --progress=false "$PI_PACKAGE"
fi
}
download_installer_artifact() {
url="$1"
output="$2"
label="$3"
if ! command -v curl >/dev/null 2>&1; then
printf 'curl is not available for the managed installer.\n' >&2
return 1
fi
http_status=$(curl -L -sS -w '%{http_code}' -o "$output" "$url") || {
rm -f "$output"
printf 'Could not download %s from %s.\n' "$label" "$url" >&2
return 1
}
if [ "$http_status" = 200 ]; then
return 0
fi
rm -f "$output"
printf 'Managed installer %s is unavailable at %s (HTTP %s).\n' "$label" "$url" "$http_status" >&2
return 1
}
managed_install_release_version() {
metadata_path="$1"
node - "$metadata_path" <<'NODE'
const fs = require("node:fs");
const metadata = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const version = metadata.version;
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version ?? "")) {
throw new Error("managed installer metadata has an invalid version");
}
console.log(version);
NODE
}
download_managed_install_artifacts() {
managed_stage_dir="$1"
printf 'Downloading managed installer release metadata\n' >&2
download_installer_artifact "$PI_INSTALLER_API_BASE/latest" "$managed_stage_dir/metadata.json" "release metadata" || return 1
if ! managed_version=$(managed_install_release_version "$managed_stage_dir/metadata.json"); then
printf 'Managed installer release metadata is invalid.\n' >&2
return 1
fi
printf 'Downloading managed installer package.json for Pi %s\n' "$managed_version" >&2
download_installer_artifact "$PI_INSTALLER_API_BASE/$managed_version/package.json" "$managed_stage_dir/package.json" "package.json" || return 1
printf 'Downloading managed installer package-lock.json for Pi %s\n' "$managed_version" >&2
download_installer_artifact "$PI_INSTALLER_API_BASE/$managed_version/package-lock.json" "$managed_stage_dir/package-lock.json" "package-lock.json" || return 1
validate_managed_install_artifacts "$managed_stage_dir/package.json" "$managed_stage_dir/package-lock.json" "$managed_version"
}
validate_managed_install_artifacts() {
package_json_path="$1"
package_lock_path="$2"
managed_version="$3"
node - "$package_json_path" "$package_lock_path" "$PI_PACKAGE" "$managed_version" <<'NODE'
const fs = require("node:fs");
const packageJson = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
const packageLock = JSON.parse(fs.readFileSync(process.argv[3], "utf8"));
const piPackage = process.argv[4];
const version = process.argv[5];
const root = packageLock.packages?.[""];
const piEntry = packageLock.packages?.[`node_modules/${piPackage}`];
if (packageJson.version !== version || packageJson.dependencies?.[piPackage] !== version) {
throw new Error(`managed installer package.json must describe ${piPackage}@${version}`);
}
if (packageLock.lockfileVersion !== 3) {
throw new Error("managed installer package-lock.json must use lockfileVersion 3");
}
if (packageLock.version !== version || root?.version !== version || root?.dependencies?.[piPackage] !== version) {
throw new Error(`managed installer package-lock.json root must describe ${piPackage}@${version}`);
}
if (piEntry?.version !== version) {
throw new Error(`managed installer package-lock.json does not include ${piPackage}@${version}`);
}
NODE
}
write_managed_install_marker() {
managed_root="$1"
managed_entrypoint_type="$2"
managed_entrypoint_path="$3"
marker_tmp="$managed_root/$PI_MANAGED_INSTALL_MARKER.tmp.$$"
node - "$marker_tmp" "$managed_entrypoint_type" "$managed_entrypoint_path" <<'NODE'
const fs = require("node:fs");
const markerPath = process.argv[2];
const entrypointType = process.argv[3];
const entrypointPath = process.argv[4];
fs.writeFileSync(
markerPath,
`${JSON.stringify(
{
kind: "pi-managed-install",
schemaVersion: 1,
layout: "releases-v1",
entrypoint: { type: entrypointType, path: entrypointPath },
},
null,
2,
)}\n`,
);
NODE
mv -f "$marker_tmp" "$managed_root/$PI_MANAGED_INSTALL_MARKER"
}
managed_install_entrypoint_path() {
managed_root="$1"
node - "$managed_root/$PI_MANAGED_INSTALL_MARKER" <<'NODE'
const fs = require("node:fs");
const marker = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
if (typeof marker.entrypoint?.path !== "string" || !marker.entrypoint.path) process.exit(1);
console.log(marker.entrypoint.path);
NODE
}
write_managed_install_launcher() {
managed_agent_dir="$1"
managed_launcher_dir="$managed_agent_dir/bin"
managed_launcher="$managed_launcher_dir/$PI_CMD"
launcher_tmp="$managed_launcher.tmp.$$"
if [ -e "$managed_launcher" ]; then
[ -f "$managed_launcher" ] && [ -x "$managed_launcher" ]
return $?
fi
mkdir -p "$managed_launcher_dir"
cat >"$launcher_tmp" <<'EOF'
#!/bin/sh
case "$0" in
*/*) pi_launcher="$0" ;;
*) pi_launcher=$(command -v "$0") || exit 127 ;;
esac
while [ -L "$pi_launcher" ]; do
pi_link=$(readlink "$pi_launcher") || exit 1
case "$pi_link" in
/*) pi_launcher="$pi_link" ;;
*) pi_launcher=${pi_launcher%/*}/$pi_link ;;
esac
done
pi_bin_dir=${pi_launcher%/*}
pi_agent_dir=${pi_bin_dir%/*}
pi_current_file=$pi_agent_dir/install/current-version
if ! IFS= read -r pi_current_version < "$pi_current_file"; then
printf 'Could not read managed Pi version from %s.\n' "$pi_current_file" >&2
exit 1
fi
case "$pi_current_version" in
""|.|..|*[!0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz._+-]*)
printf 'Managed Pi version file is invalid: %s\n' "$pi_current_file" >&2
exit 1
;;
esac
pi_release_dir=$pi_agent_dir/install/releases/$pi_current_version
pi_release_bin=$pi_release_dir/node_modules/.bin/pi
if [ ! -x "$pi_release_bin" ]; then
printf 'Managed Pi executable is missing: %s\n' "$pi_release_bin" >&2
exit 1
fi
PI_MANAGED_INSTALL_ROOT=$pi_agent_dir/install
export PI_MANAGED_INSTALL_ROOT
exec "$pi_release_bin" "$@"
EOF
chmod 755 "$launcher_tmp"
mv -f "$launcher_tmp" "$managed_launcher"
}
write_managed_install_link() {
managed_agent_dir="$1"
managed_bin_dir="$2"
managed_launcher="$managed_agent_dir/bin/$PI_CMD"
managed_entrypoint="$managed_bin_dir/$PI_CMD"
if [ "$managed_entrypoint" = "$managed_launcher" ]; then
return 0
fi
mkdir -p "$managed_bin_dir"
if [ -e "$managed_entrypoint" ] && [ ! -L "$managed_entrypoint" ]; then
printf 'Refusing to replace the executable at %s.\n' "$managed_entrypoint" >&2
return 1
fi
managed_link_target=$(node - "$managed_bin_dir" "$managed_launcher" <<'NODE'
const path = require("node:path");
console.log(path.relative(process.argv[2], process.argv[3]));
NODE
)
managed_link_tmp="$managed_bin_dir/.$PI_CMD.tmp.$$"
rm -f "$managed_link_tmp"
ln -s "$managed_link_target" "$managed_link_tmp"
mv -f "$managed_link_tmp" "$managed_entrypoint"
}
write_managed_current_version() {
managed_root="$1"
managed_version="$2"
current_tmp="$managed_root/current-version.tmp.$$"
printf '%s\n' "$managed_version" >"$current_tmp"
mv -f "$current_tmp" "$managed_root/current-version"
}
run_managed_install_pi() {
npm_loglevel="$1"
managed_root="$PI_MANAGED_INSTALL_DIR"
if [ -n "${PI_EXISTING_PATH:-}" ]; then
if ! managed_existing_root=$(managed_install_root_for_command "$PI_EXISTING_PATH") || [ "$managed_existing_root" != "$managed_root" ]; then
printf 'Experimental managed install refused to replace Pi at %s. Uninstall it first.\n' "$PI_EXISTING_PATH" >&2
return 1
fi
fi
if [ -e "$managed_root/$PI_MANAGED_INSTALL_MARKER" ] && ! managed_install_marker_is_valid "$managed_root"; then
printf 'The managed install marker at %s is invalid.\n' "$managed_root/$PI_MANAGED_INSTALL_MARKER" >&2
return 1
fi
managed_launcher="$PI_MANAGED_AGENT_DIR/bin/$PI_CMD"
if { [ -e "$managed_launcher" ] || [ -L "$managed_launcher" ]; } && ! managed_install_marker_is_valid "$managed_root"; then
printf 'Refusing to replace the unrecognized executable at %s.\n' "$managed_launcher" >&2
return 1
fi
mkdir -p "$managed_root/staging" "$managed_root/releases"
managed_stage_dir="$managed_root/staging/install-$$-$(date +%s)"
rm -rf "$managed_stage_dir"
mkdir -p "$managed_stage_dir"
if ! download_managed_install_artifacts "$managed_stage_dir"; then
rm -rf "$managed_stage_dir"
return 1
fi
printf 'Installing managed Pi dependencies\n' >&2
if (cd "$managed_stage_dir" && npm ci --ignore-scripts "$PI_NPM_INSTALL_MIN_AGE_ARG" --omit=dev --include=optional --no-fund --no-audit "--loglevel=$npm_loglevel" --progress=false); then
managed_status=0
else
managed_status=$?
fi
if [ "$managed_status" -ne 0 ]; then
rm -rf "$managed_stage_dir"
return "$managed_status"
fi
managed_staged_bin="$managed_stage_dir/node_modules/.bin/$PI_CMD"
if [ ! -x "$managed_staged_bin" ]; then
printf 'Managed Pi executable was not created by npm ci.\n' >&2
rm -rf "$managed_stage_dir"
return 1
fi
printf 'Verifying managed Pi %s\n' "$managed_version" >&2
if managed_installed_version=$("$managed_staged_bin" --version); then
managed_status=0
else
managed_status=$?
fi
if [ "$managed_status" -ne 0 ]; then
rm -rf "$managed_stage_dir"
return "$managed_status"
fi
if [ "$managed_installed_version" != "$managed_version" ]; then
printf 'Managed Pi smoke test returned version %s; expected %s.\n' "$managed_installed_version" "$managed_version" >&2
rm -rf "$managed_stage_dir"
return 1
fi
managed_release_dir="$managed_root/releases/$managed_version"
printf 'Activating managed Pi %s\n' "$managed_version" >&2
if [ -d "$managed_release_dir" ]; then
rm -rf "$managed_stage_dir"
elif ! mv "$managed_stage_dir" "$managed_release_dir"; then
rm -rf "$managed_stage_dir"
return 1
fi
if [ "$PI_MANAGED_BIN_DIR/$PI_CMD" = "$PI_MANAGED_AGENT_DIR/bin/$PI_CMD" ]; then
managed_entrypoint_type=script
else
managed_entrypoint_type=symlink
fi
write_managed_install_marker "$managed_root" "$managed_entrypoint_type" "$PI_MANAGED_BIN_DIR/$PI_CMD"
write_managed_install_launcher "$PI_MANAGED_AGENT_DIR"
write_managed_install_link "$PI_MANAGED_AGENT_DIR" "$PI_MANAGED_BIN_DIR"
write_managed_current_version "$managed_root" "$managed_version"
hash -r
printf 'Managed Pi install complete\n' >&2
}
uninstall_pi_package() {
if managed_uninstall_root=$(managed_install_root_for_command "$PI_EXISTING_PATH"); then
printf 'Uninstalling managed Pi...\n\n'
managed_uninstall_agent=${managed_uninstall_root%/*}
managed_uninstall_launcher="$managed_uninstall_agent/bin/$PI_CMD"
managed_uninstall_entrypoint=$(managed_install_entrypoint_path "$managed_uninstall_root" 2>/dev/null || true)
if [ -n "$managed_uninstall_entrypoint" ] && [ "$managed_uninstall_entrypoint" != "$managed_uninstall_launcher" ] && [ -L "$managed_uninstall_entrypoint" ]; then
rm -f "$managed_uninstall_entrypoint"
fi
if [ "$PI_EXISTING_PATH" != "$managed_uninstall_launcher" ] && [ -L "$PI_EXISTING_PATH" ]; then
rm -f "$PI_EXISTING_PATH"
fi
rm -f "$managed_uninstall_launcher"
rm -rf "$managed_uninstall_root"
hash -r
if [ -e "$PI_EXISTING_PATH" ] || [ -L "$PI_EXISTING_PATH" ]; then
printf '\nManaged uninstall finished, but pi is still present at:\n\n %s\n' "$PI_EXISTING_PATH" >&2
return 1
fi
return 0
fi
if ! npm_package_is_installed_for_uninstall; then
printf 'I found pi at:\n\n %s\n\n' "$PI_EXISTING_PATH" >&2
printf 'but npm does not show %s installed there.\n' "$PI_PACKAGE" >&2
printf 'Nothing was removed.\n' >&2
return 1
fi
printf 'Uninstalling Pi...\n\n'
run_npm_uninstall_pi error
hash -r
if [ -e "$PI_EXISTING_PATH" ] || [ -L "$PI_EXISTING_PATH" ]; then
printf '\nnpm uninstall finished, but pi is still present at:\n\n %s\n' "$PI_EXISTING_PATH" >&2
return 1
fi
}
npm_package_is_installed_for_uninstall() {
if [ -n "${PI_NPM_UNINSTALL_PREFIX:-}" ]; then
npm ls -g --prefix "$PI_NPM_UNINSTALL_PREFIX" --depth=0 "$PI_PACKAGE" >/dev/null 2>&1
else
npm ls -g --depth=0 "$PI_PACKAGE" >/dev/null 2>&1
fi
}
run_npm_uninstall_pi() {
npm_loglevel="$1"
if [ -n "${PI_NPM_UNINSTALL_PREFIX:-}" ]; then
npm uninstall -g --prefix "$PI_NPM_UNINSTALL_PREFIX" --no-fund --no-audit "--loglevel=$npm_loglevel" --progress=false "$PI_PACKAGE"
else
npm uninstall -g --no-fund --no-audit "--loglevel=$npm_loglevel" --progress=false "$PI_PACKAGE"
fi
}
install_pi_package_with_progress() {
log_file="${TMPDIR:-/tmp}/pi-installer-npm.$$"
rm -f "$log_file"
: >"$log_file"
run_pi_install verbose >"$log_file" 2>&1 &
npm_pid=$!
printf '\033[?25l'
animate_npm_install "$log_file" &
progress_pid=$!
trap 'kill "$npm_pid" 2>/dev/null || true; finish_install_progress "$progress_pid"; exit 130' INT TERM
if wait "$npm_pid"; then
status=0
else
status=$?
fi
finish_install_progress "$progress_pid"
trap - INT TERM
if [ "$status" -ne 0 ]; then
printf '\033[31mInstallation failed.\033[0m\n\n'
cat "$log_file"
rm -f "$log_file"
return "$status"
fi
rm -f "$log_file"
if terminal_supports_unicode; then
printf ' \033[32m✓\033[0m install complete\n'
else
printf ' \033[32mok\033[0m install complete\n'
fi
}
finish_install_progress() {
progress_pid="$1"
kill "$progress_pid" 2>/dev/null || true
wait "$progress_pid" 2>/dev/null || true
printf '\r\033[K\033[?25h'
}
terminal_supports_unicode() {
locale="${LC_ALL:-${LC_CTYPE:-${LANG:-}}}"
case "$locale" in
*UTF-8*|*utf-8*|*UTF8*|*utf8*) return 0 ;;
esac
case "${TERM_PROGRAM:-}" in
Apple_Terminal|iTerm.app|vscode|WezTerm) return 0 ;;
esac
return 1
}
spinner_frame() {
frame_step="$1"
frame_count="$2"
if [ "$frame_count" -eq 10 ]; then
case $((frame_step % 10)) in
0) printf '⠋' ;;
1) printf '⠙' ;;
2) printf '⠹' ;;
3) printf '⠸' ;;
4) printf '⠼' ;;
5) printf '⠴' ;;
6) printf '⠦' ;;
7) printf '⠧' ;;
8) printf '⠇' ;;
*) printf '⠏' ;;
esac
else
case $((frame_step % 4)) in
0) printf '-' ;;
1) printf '\\' ;;
2) printf '|' ;;
*) printf '/' ;;
esac
fi
}
animate_npm_install() {
log_file="$1"
if terminal_supports_unicode; then
full="█"
empty="░"
frame_count=10
else
full="#"
empty="-"
frame_count=4
fi
step=0
if pi_managed_install_enabled; then
label="starting managed install"
else
label="starting npm install"
fi
while :; do
frame=$(spinner_frame "$step" "$frame_count")
if [ $((step % 5)) -eq 0 ]; then
label=$(npm_install_progress_label "$log_file" "$label")
fi
draw_install_progress "$step" "$frame" "$label" "$full" "$empty"
step=$((step + 1))
sleep 0.08
done
}
animate_node_install() {
log_file="$1"
method_label="$2"
if terminal_supports_unicode; then
full="█"
empty="░"
frame_count=10
else
full="#"
empty="-"
frame_count=4
fi
step=0
label="starting ${method_label} install"
while :; do
frame=$(spinner_frame "$step" "$frame_count")
if [ $((step % 5)) -eq 0 ]; then
label=$(node_install_progress_label "$log_file" "$label")
fi
draw_install_progress "$step" "$frame" "$label" "$full" "$empty" "Installing Node.js"
step=$((step + 1))
sleep 0.08
done
}
node_install_progress_label() {
log_file="$1"
label="$2"
while IFS= read -r line; do
line=${line##*"$PI_CR"}
case "$line" in
"") ;;
Resolving\ Node.js*) label="resolving Node.js binary" ;;
Downloading\ Node.js*) label="$line" ;;
Verifying\ Node.js*) label="verifying download" ;;
Installing\ xz-utils*) label="installing xz-utils" ;;
Extracting\ Node.js*) label="extracting Node.js" ;;
Node.js\ installed*) label="Node.js installed" ;;
Hit:*|Get:*|Ign:*) label="updating package lists" ;;
Reading\ package\ lists*) label="reading package lists" ;;
Building\ dependency\ tree*) label="resolving dependencies" ;;
The\ following\ NEW\ packages*) label="installing dependencies" ;;
Need\ to\ get*|Fetched\ *) label="$line" ;;
Selecting\ previously\ unselected\ package*) label="selecting packages" ;;
Preparing\ to\ unpack*) label="preparing packages" ;;
Unpacking\ *|Setting\ up\ *) label="$line" ;;
fetch\ *) label="fetching packages" ;;
*Installing\ nodejs*) label="$line" ;;
OK:\ *) label="$line" ;;
==\>\ Downloading*) label="downloading packages" ;;
==\>\ Installing*|==\>\ Upgrading*) label="$line" ;;
==\>\ Pouring*) label="installing package" ;;
*already\ installed*) label="$line" ;;
esac
done < "$log_file"
if [ "${#label}" -gt 64 ]; then
label=$(printf '%.61s...' "$label")
fi
printf '%s' "$label"
}
npm_install_progress_label() {
log_file="$1"
label="$2"
metadata_cache_count=0
metadata_fetch_count=0
tarball_cache_count=0
tarball_fetch_count=0
while IFS= read -r line; do
line=${line%"$PI_CR"}
case "$line" in
Downloading\ managed\ installer\ release\ metadata*)
label="resolving managed release"
;;
Downloading\ managed\ installer\ package.json*)
label="fetching managed package manifest"
;;
Downloading\ managed\ installer\ package-lock.json*)
label="fetching managed package lock"
;;
Installing\ managed\ Pi\ dependencies*)
label="installing managed dependencies"
;;
Verifying\ managed\ Pi*)
label="verifying managed package"
;;
Activating\ managed\ Pi*)
label="activating managed package"
;;
Managed\ Pi\ install\ complete*)
label="managed install complete"
;;
npm\ verbose\ title\ npm\ install*|npm\ verbose\ title\ npm\ ci*)
label="resolving packages"
;;
npm\ http\ fetch\ GET\ *https://registry.npmjs.org/*.tgz*)
tarball_fetch_count=$((tarball_fetch_count + 1))
label="fetching tarballs (${tarball_fetch_count})"
;;
npm\ http\ cache\ *@https://registry.npmjs.org/*.tgz*)
tarball_cache_count=$((tarball_cache_count + 1))
if [ "$tarball_fetch_count" -gt 0 ]; then
label="fetching tarballs (${tarball_fetch_count})"
else
label="checking tarballs (${tarball_cache_count})"
fi
;;
npm\ http\ fetch\ GET\ *https://registry.npmjs.org/*)
metadata_fetch_count=$((metadata_fetch_count + 1))
label="fetching package metadata (${metadata_fetch_count})"
;;
npm\ http\ cache\ https://registry.npmjs.org/*)
metadata_cache_count=$((metadata_cache_count + 1))
if [ "$metadata_fetch_count" -gt 0 ]; then
label="fetching package metadata (${metadata_fetch_count})"
else
label="checking cached metadata (${metadata_cache_count})"
fi
;;
npm\ info\ run\ *)
rest=${line#npm info run }
package=${rest%% *}
rest=${rest#* }
script=${rest%% *}
package=${package%@*}
case "$line" in
*\{\ code:*) label="finished ${script} for ${package}" ;;
*) label="running ${script} for ${package}" ;;
esac
;;
changed\ *|added\ *|removed\ *|updated\ *|up\ to\ date\ *)
label="$line"
;;
esac
done < "$log_file"
printf '%s' "$label"
}
draw_install_progress() {
step="$1"; frame="$2"; label="$3"; full="$4"; empty="$5"; title="${6:-Installing Pi}"
reset="${PI_ESC}[0m"
dim="${PI_ESC}[2m"
cyan="${PI_ESC}[36m"
red="${PI_ESC}[31m"
green="${PI_ESC}[32m"
orange="${PI_ESC}[33m"
bold="${PI_ESC}[1m"
width=28
trail=8
head=$((step % (width + trail)))
bar=""
i=0
while [ "$i" -lt "$width" ]; do
age=$((head - i))
if [ "$age" -ge 0 ] && [ "$age" -lt "$trail" ]; then
case "$age" in
0|1) cell="${green}${full}${reset}" ;;
2|3) cell="${cyan}${full}${reset}" ;;
4|5) cell="${red}${full}${reset}" ;;
*) cell="${orange}${full}${reset}" ;;
esac
else
cell="${dim}${empty}${reset}"
fi
bar="${bar}${cell}"
i=$((i + 1))
done
printf '\r\033[K %s%s%s %s %s%s%s %s' "$orange" "$frame" "$reset" "$bar" "$bold" "$title" "$reset" "$label"
}
pi_logo_animation() {
if [ ! -t 1 ] || [ "${TERM:-}" = "dumb" ]; then
print_static_logo
return
fi
esc="${PI_ESC}["
reset="${PI_ESC}[0m"
hide="${esc}?25l"
show="${esc}?25h"
clear="${esc}H"
trap 'printf "%s%s\n" "$reset" "$show"; trap - INT TERM; exit 130' INT TERM
printf '%s%s' "$hide" "${esc}2J${esc}H"
for y in 0 1 2 3; do draw_logo_frame "$clear" "$reset" 0 left 2 "$y" 0 0; sleep 0.075; done
for y in 0 1 2; do draw_logo_frame "$clear" "$reset" 1 top 2 "$y" 0 0; sleep 0.075; done
for y in 0 1 2 3 4; do draw_logo_frame "$clear" "$reset" 2 right 5 "$y" 0 0; sleep 0.075; done
draw_logo_frame "$clear" "$reset" 3 none 0 0 0 0; sleep 0.25
draw_logo_frame "$clear" "$reset" 3 none 0 0 1 0; sleep 0.08
draw_logo_frame "$clear" "$reset" 3 none 0 0 0 0; sleep 0.08
draw_logo_frame "$clear" "$reset" 3 none 0 0 1 0; sleep 0.08
draw_logo_frame "$clear" "$reset" 4 none 0 0 0 0; sleep 0.10
draw_logo_frame "$clear" "$reset" 5 none 0 0 0 0; sleep 0.45
draw_logo_frame "$clear" "$reset" 5 none 0 0 0 1; sleep 0.12
draw_logo_frame "$clear" "$reset" 5 none 0 0 0 0; sleep 0.12
draw_logo_frame "$clear" "$reset" 5 none 0 0 0 1; sleep 0.45
printf '%s%s\n' "$reset" "$show"
trap - INT TERM
}
draw_logo_frame() {
clear="$1"; reset="$2"; phase="$3"; active="$4"; ax="$5"; ay="$6"; flash="$7"; white="$8"
left=0
top=0
panel_cell="${reset} "
cyan_cell="${PI_ESC}[36m██"
red_cell="${PI_ESC}[31m██"
green_cell="${PI_ESC}[32m██"
orange_cell="${PI_ESC}[33m██"
white_cell="${PI_ESC}[39m██"
flash_cell="${PI_ESC}[33m██"
pad=$(repeat_space "$left")
clear_cell="$panel_cell"
frame="$clear"
i=0
while [ "$i" -lt "$top" ]; do frame="${frame}\n"; i=$((i + 1)); done
for y in 0 1 2 3 4 5 6 7 8; do
frame="${frame}${pad}"
for x in 1 2 3 4 5 6 7 8; do
set_logo_cell_color "$phase" "$active" "$ax" "$ay" "$flash" "$white" "$y" "$x"
case "$LOGO_COLOR" in
cyan) cell="$cyan_cell" ;;
red) cell="$red_cell" ;;
green) cell="$green_cell" ;;
orange) cell="$orange_cell" ;;
white) cell="$white_cell" ;;
flash) cell="$flash_cell" ;;
*) cell="$clear_cell" ;;
esac
frame="${frame}${cell}"
done
frame="${frame}${reset}\n"
done
printf '%b' "$frame" 2>/dev/null || true
}
set_logo_cell_color() {
phase="$1"; active="$2"; ax="$3"; ay="$4"; flash="$5"; white="$6"; y="$7"; x="$8"
if [ "$white" = 1 ]; then
if in_cells "$y" "$x" "3,2 3,3 3,4 4,2 4,4 5,2 5,3 5,5 6,2 6,5"; then LOGO_COLOR=white; else LOGO_COLOR=panel; fi
return
fi
if [ "$flash" = 1 ] && [ "$y" = 6 ] && [ "$x" -ge 1 ] && [ "$x" -le 6 ]; then LOGO_COLOR=flash; return; fi
case "$active" in
left) if in_piece "$y" "$x" "$ay" "$ax" "0,0 1,0 1,1 2,0"; then LOGO_COLOR=red; return; fi ;;
top) if in_piece "$y" "$x" "$ay" "$ax" "0,0 0,1 0,2 1,2"; then LOGO_COLOR=cyan; return; fi ;;
right) if in_piece "$y" "$x" "$ay" "$ax" "0,0 1,0 2,0 2,1"; then LOGO_COLOR=green; return; fi ;;
esac
if [ "$phase" = 4 ]; then
if in_cells "$y" "$x" "2,2 2,3 2,4 3,4"; then LOGO_COLOR=cyan; return; fi
if in_cells "$y" "$x" "3,2 4,2 4,3 5,2"; then LOGO_COLOR=red; return; fi
if in_cells "$y" "$x" "4,5 5,5"; then LOGO_COLOR=green; return; fi
LOGO_COLOR=panel; return
fi
if [ "$phase" -ge 5 ]; then
if in_cells "$y" "$x" "3,2 3,3 3,4 4,4"; then LOGO_COLOR=cyan; return; fi
if in_cells "$y" "$x" "4,2 5,2 5,3 6,2"; then LOGO_COLOR=red; return; fi
if in_cells "$y" "$x" "5,5 6,5"; then LOGO_COLOR=green; return; fi
LOGO_COLOR=panel; return
fi
if [ "$phase" -le 3 ] && in_cells "$y" "$x" "6,1 6,2 6,3 6,4"; then LOGO_COLOR=orange; return; fi
if [ "$phase" -ge 2 ] && in_cells "$y" "$x" "2,2 2,3 2,4 3,4"; then LOGO_COLOR=cyan; return; fi
if [ "$phase" -ge 1 ] && in_cells "$y" "$x" "3,2 4,2 4,3 5,2"; then LOGO_COLOR=red; return; fi
if [ "$phase" -ge 3 ] && in_cells "$y" "$x" "4,5 5,5 6,5 6,6"; then LOGO_COLOR=green; return; fi
LOGO_COLOR=panel
}
in_piece() {
y="$1"; x="$2"; py="$3"; px="$4"; cells="$5"
for item in $cells; do
dy=${item%,*}; dx=${item#*,}
[ "$y" -eq $((py + dy)) ] && [ "$x" -eq $((px + dx)) ] && return 0
done
return 1
}
in_cells() {
y="$1"; x="$2"; shift 2
for item in $1; do
[ "$item" = "$y,$x" ] && return 0
done
return 1
}
repeat_space() {
count="$1"; out=""
while [ "$count" -gt 0 ]; do out=" $out"; count=$((count - 1)); done
printf '%s' "$out"
}
print_static_logo() {
cat <<'EOF'
██████
██ ██
████ ██
██ ██
EOF
}
pi_installer_main "$@"