#!/usr/bin/env bash # install.sh — install pi-extensions # # Symlinks each extension in extensions/ into ~/.pi/agent/extensions/ so pi # loads them automatically on every session. Idempotent and non-destructive. # # Usage: # ./install.sh install all extensions # ./install.sh --only ssh-controlmaster install one extension # ./install.sh --only "ext1,ext2" install a subset # ./install.sh --skip "ext1,ext2" install all except these # ./install.sh --yes skip confirmation prompt # ./install.sh --uninstall remove symlinks that point into this repo set -euo pipefail SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" EXTENSIONS_SRC="${SCRIPT_DIR}/extensions" EXTENSIONS_DEST="${HOME}/.pi/agent/extensions" PI_AGENT_DIR="${HOME}/.pi/agent" # ── helpers ────────────────────────────────────────── ok() { printf ' \e[32m✓\e[0m %s\n' "$*"; } note() { printf '==> %s\n' "$*"; } warn() { printf ' \e[33m!\e[0m %s\n' "$*" >&2; } err() { printf ' \e[31m✗\e[0m %s\n' "$*" >&2; } confirm() { [[ "$ASSUME_YES" == "yes" ]] && return 0 read -r -p "Proceed? [y/N] " ans [[ "$ans" =~ ^[Yy]$ ]] } link_into_repo() { local target [[ -L "$1" ]] || return 1 target=$(readlink -f "$1" 2>/dev/null || true) [[ "$target" == "$SCRIPT_DIR"/* ]] } require_pi_installed() { if [[ ! -d "$PI_AGENT_DIR" ]]; then err "pi not detected at $PI_AGENT_DIR" printf ' Install pi first: https://github.com/mariozechner/pi-coding-agent\n' printf ' Re-run after `pi --help` (first run creates ~/.pi/agent/).\n' exit 4 fi mkdir -p "$EXTENSIONS_DEST" ok "pi detected at $PI_AGENT_DIR" } # ── args ───────────────────────────────────────────── ACTION="install" ASSUME_YES="no" ONLY="" # comma-separated names to include (empty = all) SKIP="" # comma-separated names to exclude (empty = none) while [[ $# -gt 0 ]]; do case "$1" in --uninstall) ACTION="uninstall"; shift ;; -y|--yes) ASSUME_YES="yes"; shift ;; --only) ONLY="$2"; shift 2 ;; --only=*) ONLY="${1#--only=}"; shift ;; --skip) SKIP="$2"; shift 2 ;; --skip=*) SKIP="${1#--skip=}"; shift ;; -h|--help) cat <&2; exit 2 ;; esac done # ── build install set ───────────────────────────────── # INSTALL_SET: space-delimited bare names (no .ts suffix). Bash 3 compatible. INSTALL_SET="" in_install_set() { [[ " $INSTALL_SET " == *" $1 "* ]]; } build_install_set() { local n f bare entry new if [[ -n "$ONLY" ]]; then # Explicit allowlist — only install what's named IFS=',' read -ra names <<< "$ONLY" for n in "${names[@]}"; do INSTALL_SET="${INSTALL_SET:+$INSTALL_SET }${n%.ts}" done else # Start with everything present on disk, then remove --skip entries for f in "${EXTENSIONS_SRC}"/*.ts; do [[ -e "$f" ]] && INSTALL_SET="${INSTALL_SET:+$INSTALL_SET }$(basename "$f" .ts)" done if [[ -n "$SKIP" ]]; then IFS=',' read -ra names <<< "$SKIP" for n in "${names[@]}"; do bare="${n%.ts}" new="" for entry in $INSTALL_SET; do [[ "$entry" == "$bare" ]] || new="${new:+$new }$entry" done INSTALL_SET="$new" done fi fi } # ── install ────────────────────────────────────────── do_install() { echo echo "pi-extensions installer" echo "Repository: $SCRIPT_DIR" echo require_pi_installed build_install_set if [[ -z "$INSTALL_SET" ]]; then warn "No extensions selected — nothing to install." exit 0 fi echo "==> Extensions to symlink into ${EXTENSIONS_DEST}/:" for n in $INSTALL_SET; do printf ' %s.ts\n' "$n" done echo confirm || { echo "Aborted."; exit 0; } echo for src in "${EXTENSIONS_SRC}"/*.ts; do [[ -e "$src" ]] || continue local name name="$(basename "$src")" local bare="${name%.ts}" in_install_set "$bare" || continue local dest="${EXTENSIONS_DEST}/${name}" local disabled="${dest}.off" note "Linking ${name}" # Respect a prior /ext disable: if .ts.off exists and points # into this repo, leave it alone. ext-toggle will flip it back. if [[ -L "$disabled" ]] && link_into_repo "$disabled"; then ok "${name} kept disabled (${name}.off present)" continue fi if [[ -e "$dest" || -L "$dest" ]]; then if link_into_repo "$dest"; then ok "${name} already linked" continue fi local backup="${dest}.bak.$(date +%Y%m%d-%H%M%S)" mv "$dest" "$backup" warn "Existing ${dest} backed up to ${backup}" fi ln -s "$src" "$dest" ok "Linked ${name} → ${src}" done echo ok "Done. Reload pi with /reload or restart to pick up new extensions." } # ── uninstall ──────────────────────────────────────── do_uninstall() { echo echo "pi-extensions uninstaller" echo "Repository: $SCRIPT_DIR" echo confirm || { echo "Aborted."; exit 0; } echo local removed=0 # Match both active (.ts) and disabled (.ts.off) symlinks — the ext-toggle # extension can rename a link to .ts.off to disable it, and uninstall # should still clean those up. for pattern in "*.ts" "*.ts.off"; do for dest in "${EXTENSIONS_DEST}"/$pattern; do [[ -e "$dest" || -L "$dest" ]] || continue if link_into_repo "$dest"; then rm "$dest" ok "Removed $(basename "$dest")" (( removed++ )) || true fi done done if [[ $removed -eq 0 ]]; then ok "No symlinks pointing into this repo found — nothing removed." else echo ok "Done. Removed ${removed} symlink(s)." fi } case "$ACTION" in install) do_install ;; uninstall) do_uninstall ;; esac