d2b2b3fb43
extensions/ext-toggle.ts: /ext lists ~/.pi/agent/extensions/ with active/disabled markers and toggles individual extensions by renaming between name.ts and name.ts.off (pi only auto-discovers *.ts). Calls ctx.reload() so the change takes effect without restarting pi. Subdirectory-style extensions (name/index.ts) are listed read-only in v1 — toggling a directory cleanly is more work than the rename trick is worth. install.sh: --uninstall now matches both *.ts and *.ts.off symlinks pointing into this repo, so a disabled extension is still cleaned up. README.md / AGENTS.md: Document ext-toggle alongside the others; AGENTS notes the API surface used (registerCommand, ui.select/confirm/notify, reload) and the rename-not-delete design decision.
211 lines
7.0 KiB
Bash
Executable File
211 lines
7.0 KiB
Bash
Executable File
#!/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 <<EOF
|
|
install.sh — install pi-extensions
|
|
|
|
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
|
|
|
|
--only and --skip accept comma-separated extension names (without .ts suffix).
|
|
--only takes precedence over --skip if both are given.
|
|
|
|
Each .ts file in extensions/ is symlinked into ~/.pi/agent/extensions/.
|
|
Existing files already symlinked into this repo are left alone.
|
|
Existing real files or foreign symlinks are backed up with a timestamp.
|
|
EOF
|
|
exit 0 ;;
|
|
*) echo "Unknown flag: $1" >&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}"
|
|
|
|
note "Linking ${name}"
|
|
|
|
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
|