Initial commit: pi harness bring-up split out of mempalace-toolkit

Pi-generic config artifacts (no mempalace dependency):
- pi-env.zsh: shell loader sourcing ~/.config/pi/.env for AWS_PROFILE /
  AWS_REGION. POSIX-compatible (works in bash and zsh).
- keybindings.json: mosh/tmux newline bindings (shift+enter, ctrl+j, alt+j).
- settings.example.json: ~/.pi/agent/settings.json template so pi starts
  without --provider/--model. Region-specific (Bedrock inference-profile
  prefix).

install.sh mirrors mempalace-toolkit + opencode-toolkit patterns:
- require_pi_installed: hard exit 4 if ~/.pi/agent/ missing (cannot do
  anything useful; user must install pi first).
- symlink keybindings.json (safe: pi doesn't rewrite it).
- cp pi-env.zsh into ~/.oh-my-zsh/custom/ (portability over symlink,
  that dir is part of dotfiles backups). Print source snippet for bash
  / plain-zsh users.
- settings.example.json NOT installed \u2014 pi rewrites settings.json at
  runtime. check_pi_settings probe prints the cp command instead.
- check_aws_env: gated on settings.json selecting amazon-bedrock; silent
  for non-Bedrock providers or missing settings.
- All probes warn + return 0, never halt.
- Non-destructive: backup on symlink collision, cmp-based drift detection
  on the cp path, uninstall only removes copies whose content still
  matches repo.

Split rationale: opencode-devbox's mempalace opt-out (~300 MB saved)
wants pi available without mempalace. That dependency asymmetry is
cleanest when pi's own config lives in its own repo, same shape as
opencode-toolkit split out earlier today.

The pi\u2194mempalace MCP bridge (mempalace.ts) stays in mempalace-toolkit
where it belongs \u2014 it imports pi's ExtensionAPI but only exists to
bridge to the palace.

Verified on tor-ms22: fresh install \u2192 drift detect \u2192 adopt canonical \u2192
uninstall \u2192 reinstall \u2192 zsh -ic loads AWS vars. Bash fallback path also
tested via HOME=/tmp/fake SHELL=/bin/bash.
This commit is contained in:
2026-05-05 17:22:06 +02:00
commit ff774cf88f
7 changed files with 762 additions and 0 deletions
Executable
+276
View File
@@ -0,0 +1,276 @@
#!/usr/bin/env bash
# install.sh — install pi-toolkit
#
# Harness-side bring-up for the pi coding-agent (https://github.com/mariozechner/pi-coding-agent):
# - pi-env.zsh : shell loader sourcing ~/.config/pi/.env so AWS_PROFILE
# and AWS_REGION are in every shell that launches pi.
# - keybindings.json : mosh/tmux-friendly newline bindings
# (shift+enter, ctrl+j, alt+j).
# - settings.example.json : template for ~/.pi/agent/settings.json so pi
# starts without --provider/--model. Copy + edit.
#
# No dependency on MemPalace. For the palace bridge see
# https://gitea.jordbo.se/joakimp/mempalace-toolkit (optional).
#
# Idempotent. Safe to re-run. Non-destructive on drift: if installed content
# differs from repo, leaves user edits alone and points at diff.
set -euo pipefail
# ── locate self ──────────────────────────────────────
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# ── targets ──────────────────────────────────────────
PI_EXT_DEST_DIR="${HOME}/.pi/agent/extensions" # existence = pi is installed
PI_AGENT_DIR="${HOME}/.pi/agent"
# keybindings — symlinked (pi doesn't rewrite it at runtime)
PI_KEYS_SRC="${SCRIPT_DIR}/keybindings.json"
PI_KEYS_DEST="${HOME}/.pi/agent/keybindings.json"
# settings template — NOT installed, only referenced (pi rewrites settings.json)
PI_SETTINGS_EXAMPLE="${SCRIPT_DIR}/settings.example.json"
PI_SETTINGS_DEST="${HOME}/.pi/agent/settings.json"
# shell env loader — cp'd into oh-my-zsh custom dir (portability over symlink)
PI_ENV_SRC="${SCRIPT_DIR}/pi-env.zsh"
PI_ENV_OMZ_DEST="${HOME}/.oh-my-zsh/custom/pi-env.zsh"
# ── args ─────────────────────────────────────────────
ACTION="install"
ASSUME_YES="no"
while [[ $# -gt 0 ]]; do
case "$1" in
--uninstall) ACTION="uninstall"; shift ;;
-y|--yes) ASSUME_YES="yes"; shift ;;
-h|--help)
cat <<EOF
install.sh — install pi-toolkit
Usage:
./install.sh # install (interactive confirm)
./install.sh --yes # install without prompt
./install.sh --uninstall # remove symlinks and copied loaders
What install does:
- Requires pi installed (~/.pi/agent/ must exist). Aborts cleanly otherwise.
- Symlinks keybindings.json into ~/.pi/agent/keybindings.json.
- If oh-my-zsh is present (~/.oh-my-zsh/custom/), copies pi-env.zsh there
so every new zsh shell sources ~/.config/pi/.env. Uses cp (not symlink)
for dotfile-backup portability. Prints source snippet otherwise.
- Probes (non-halting warnings): settings.json present, and AWS_PROFILE /
AWS_REGION set if settings.json selects amazon-bedrock.
What uninstall does:
- Removes keybindings.json symlink if it points into this repo.
- Removes pi-env.zsh copy ONLY if its content still matches the repo.
- Never touches settings.json (it's your file, not managed here).
No dependency on MemPalace. For memory integration see:
https://gitea.jordbo.se/joakimp/mempalace-toolkit
EOF
exit 0 ;;
*) echo "Unknown flag: $1" >&2; exit 2 ;;
esac
done
# ── 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_if_into_repo() {
# Return 0 if $1 is a symlink pointing into $SCRIPT_DIR
local target
[[ -L "$1" ]] || return 1
target=$(readlink -f "$1")
[[ "$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 this installer after `pi` has been started at least once\n'
printf ' (first run creates ~/.pi/agent/).\n'
exit 4
fi
# Ensure the extensions dir exists so pi loads any extensions we may add
# later (e.g. mempalace-toolkit's mempalace.ts). pi creates this on first
# extension install but we may race ahead of that.
mkdir -p "$PI_EXT_DEST_DIR"
ok "pi detected at $PI_AGENT_DIR"
}
# ── install ──────────────────────────────────────────
install_keybindings() {
# Generic mosh/tmux newline fix. Symlinked so edits flow through git.
note "Linking keybindings.json → $PI_KEYS_DEST"
if [[ -e "$PI_KEYS_DEST" || -L "$PI_KEYS_DEST" ]]; then
if link_if_into_repo "$PI_KEYS_DEST"; then
ok "pi keybindings already linked"
return 0
fi
# Back up any existing real file / foreign symlink.
local backup="${PI_KEYS_DEST}.bak.$(date +%Y%m%d-%H%M%S)"
mv "$PI_KEYS_DEST" "$backup"
warn "Existing $PI_KEYS_DEST backed up to $backup"
fi
ln -s "$PI_KEYS_SRC" "$PI_KEYS_DEST"
ok "Linked keybindings.json → $PI_KEYS_SRC"
}
install_env_loader() {
# Copy pi-env.zsh into ~/.oh-my-zsh/custom/ (auto-loaded by omz).
# cp not symlink because that dir is typically part of a dotfiles backup
# and a symlink into this repo breaks on restore to another host.
# No oh-my-zsh → print a source snippet for manual rc-file addition.
if [[ -d "$HOME/.oh-my-zsh/custom" ]]; then
note "Installing pi-env.zsh into ~/.oh-my-zsh/custom/"
if [[ -f "$PI_ENV_OMZ_DEST" ]]; then
if cmp -s "$PI_ENV_SRC" "$PI_ENV_OMZ_DEST"; then
ok "pi-env.zsh already installed (content matches repo)"
return 0
fi
warn "$PI_ENV_OMZ_DEST exists and differs from repo copy"
printf ' Leaving your edits alone. Compare with:\n'
printf ' diff %q %q\n' "$PI_ENV_OMZ_DEST" "$PI_ENV_SRC"
printf ' To adopt the repo version: rm that file and re-run install.sh\n'
return 0
fi
cp "$PI_ENV_SRC" "$PI_ENV_OMZ_DEST"
ok "Copied pi-env.zsh → $PI_ENV_OMZ_DEST"
printf ' Open a new shell (or `exec zsh`) to load AWS_PROFILE / AWS_REGION.\n'
return 0
fi
# No oh-my-zsh — print shell-specific snippet.
note "oh-my-zsh not detected — manual shell setup needed"
local shell_name
shell_name="$(basename "${SHELL:-bash}")"
local rc_file
case "$shell_name" in
zsh) rc_file="~/.zshrc" ;;
bash) rc_file="~/.bashrc" ;;
*) rc_file="~/.${shell_name}rc # adjust for your shell" ;;
esac
printf ' Add this line to %s so every shell sources ~/.config/pi/.env:\n' "$rc_file"
printf ' source %q\n' "$PI_ENV_SRC"
printf ' (the loader itself is POSIX-compatible — works in bash and zsh.)\n'
}
# ── Probes ───────────────────────────────────────────
# Never halt. warn + return 0 — mirrors opencode-toolkit and mempalace-toolkit.
check_pi_settings() {
# If settings.json missing, pi refuses to start without --provider/--model
# on every run. Template exists in this repo; point at the cp command.
if [[ -f "$PI_SETTINGS_DEST" ]]; then
ok "pi settings.json present at $PI_SETTINGS_DEST"
return 0
fi
warn "pi settings.json NOT found at $PI_SETTINGS_DEST"
printf ' Without it, pi must be invoked with --provider/--model every run.\n'
printf ' Bootstrap from the shipped template:\n'
printf ' cp %q %q\n' "$PI_SETTINGS_EXAMPLE" "$PI_SETTINGS_DEST"
printf ' $EDITOR %q # adjust region prefix + model IDs\n' "$PI_SETTINGS_DEST"
printf ' See README.md for the eu./us./anthropic: prefix rules.\n'
}
check_aws_env() {
# Only warn if settings.json selects amazon-bedrock. If pi uses a
# non-Bedrock provider (bare anthropic, openai, ...) AWS creds are
# irrelevant and this probe would be noise. If settings.json doesn't
# exist yet, check_pi_settings already told the user to bootstrap it
# — we can't know which provider they'll pick, so stay quiet here.
[[ -f "$PI_SETTINGS_DEST" ]] || return 0
grep -q '"amazon-bedrock"' "$PI_SETTINGS_DEST" 2>/dev/null || return 0
if [[ -n "${AWS_PROFILE:-}" && -n "${AWS_REGION:-}" ]]; then
ok "AWS env present (AWS_PROFILE=$AWS_PROFILE, AWS_REGION=$AWS_REGION)"
return 0
fi
warn "AWS_PROFILE and/or AWS_REGION not set in this shell"
printf ' pi with defaultProvider=amazon-bedrock needs both to invoke Bedrock.\n'
printf ' Recommended layout (matches tor-ms22 dotfiles pattern):\n'
printf ' ~/.config/pi/.env # AWS_PROFILE=..., AWS_REGION=...\n'
printf ' The pi-env.zsh loader (installed above) sources it automatically.\n'
printf ' Open a fresh shell to pick up the vars after creating the file.\n'
}
do_install() {
echo
echo "pi-toolkit installer"
echo "Repository: $SCRIPT_DIR"
echo
require_pi_installed
echo
echo "==> Installation plan:"
echo " Symlink keybindings.json → $PI_KEYS_DEST"
if [[ -d "$HOME/.oh-my-zsh/custom" ]]; then
echo " Copy pi-env.zsh → $PI_ENV_OMZ_DEST"
else
echo " Print manual source-snippet instructions for pi-env.zsh"
fi
echo
confirm || { echo "Aborted."; exit 0; }
echo
install_keybindings
echo
install_env_loader
echo
check_pi_settings
echo
check_aws_env
echo
ok "Done."
}
# ── uninstall ────────────────────────────────────────
do_uninstall() {
echo
echo "pi-toolkit uninstaller"
echo "Repository: $SCRIPT_DIR"
echo
confirm || { echo "Aborted."; exit 0; }
echo
note "Removing pi keybindings symlink"
if link_if_into_repo "$PI_KEYS_DEST"; then
rm "$PI_KEYS_DEST"
ok "Removed pi keybindings symlink"
else
ok "No pi keybindings symlink to remove"
fi
echo
note "Removing pi-env.zsh loader (oh-my-zsh path)"
# Only remove if content still matches the repo copy — user may have
# local edits we shouldn't silently discard.
if [[ -f "$PI_ENV_OMZ_DEST" ]] && cmp -s "$PI_ENV_SRC" "$PI_ENV_OMZ_DEST"; then
rm "$PI_ENV_OMZ_DEST"
ok "Removed $PI_ENV_OMZ_DEST"
elif [[ -f "$PI_ENV_OMZ_DEST" ]]; then
warn "$PI_ENV_OMZ_DEST differs from repo copy — left alone"
else
ok "No pi-env.zsh loader to remove"
fi
echo
ok "Done."
}
case "$ACTION" in
install) do_install ;;
uninstall) do_uninstall ;;
esac