#!/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 "$@"